transloadit 4.11.1 → 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/ai-chat.d.ts +2 -1
- package/dist/alphalib/types/robots/ai-chat.d.ts.map +1 -1
- package/dist/alphalib/types/robots/ai-chat.js +6 -5
- package/dist/alphalib/types/robots/ai-chat.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/ai-chat.ts +6 -5
- 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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { lstat, mkdir, open, rm } from 'node:fs/promises'
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { validateStoragePathPrefix } from '@transloadit/utils'
|
|
5
|
+
import { Command, Option } from 'clipanion'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
import { noticeCliCredentialSource, resolveCliConfig } from '../helpers.ts'
|
|
9
|
+
import { resolveStorageWorkspace } from '../storageCatalog.ts'
|
|
10
|
+
import { storagePublicError } from '../storagePublic.ts'
|
|
11
|
+
import {
|
|
12
|
+
assertStorageCatalogOrigin,
|
|
13
|
+
defaultStorageCatalog,
|
|
14
|
+
readStorageCatalog,
|
|
15
|
+
storageCatalogDelivery,
|
|
16
|
+
storageTypesPath,
|
|
17
|
+
updateStorageReceipts,
|
|
18
|
+
} from '../storageReceipts.ts'
|
|
19
|
+
import { nextAppRoot, storageImageEnvBlock, storageImagePage } from '../storageSnippets.ts'
|
|
20
|
+
import { ensureError, isErrnoException } from '../types.ts'
|
|
21
|
+
import { UnauthenticatedCommand } from './BaseCommand.ts'
|
|
22
|
+
|
|
23
|
+
/** Scaffolds the Node-runtime Next.js integration without overwriting code or env files. */
|
|
24
|
+
export class ImageInitCommand extends UnauthenticatedCommand {
|
|
25
|
+
static override paths = [['image', 'init']]
|
|
26
|
+
static override usage = Command.Usage({
|
|
27
|
+
category: 'Storage',
|
|
28
|
+
description: 'Optionally scaffold a Storage image example or private authorizer',
|
|
29
|
+
examples: [['Scaffold an example', 'transloadit image init website/ --example']],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
privateDelivery = Option.Boolean('--private', false, {
|
|
33
|
+
description: 'Also generate a redirect route; denies access until you supply authorization',
|
|
34
|
+
})
|
|
35
|
+
example = Option.Boolean('--example', false, {
|
|
36
|
+
description: 'Generate an example page; explicit factories remain an optional escape hatch',
|
|
37
|
+
})
|
|
38
|
+
publicDelivery = Option.Boolean('--public', false, {
|
|
39
|
+
description: 'Publish this directory on the server and use permanent unsigned image URLs',
|
|
40
|
+
})
|
|
41
|
+
receipts = Option.String('--receipts', defaultStorageCatalog, {
|
|
42
|
+
description: 'Rendering catalog selected by withTransloaditImages',
|
|
43
|
+
})
|
|
44
|
+
writeEnv = Option.Boolean('--write-env', false, {
|
|
45
|
+
description: 'Copy an imported application key to an owner-only .env.local; never overwrite it',
|
|
46
|
+
})
|
|
47
|
+
prefix = Option.String({ required: true })
|
|
48
|
+
workspace = Option.String('--workspace', {
|
|
49
|
+
description: 'Workspace expected for the selected credentials',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
protected async run(): Promise<number | undefined> {
|
|
53
|
+
const created: string[] = []
|
|
54
|
+
let createdCatalog = false
|
|
55
|
+
let published: string | undefined
|
|
56
|
+
try {
|
|
57
|
+
if (this.privateDelivery && this.publicDelivery)
|
|
58
|
+
throw new Error('Choose either --private or --public, not both')
|
|
59
|
+
if (!this.privateDelivery && !this.publicDelivery && !this.example)
|
|
60
|
+
throw new Error('Choose --example or --private; normal uploads need only storage store')
|
|
61
|
+
const prefix =
|
|
62
|
+
this.prefix.endsWith('/') || this.prefix === '' ? this.prefix : `${this.prefix}/`
|
|
63
|
+
try {
|
|
64
|
+
validateStoragePathPrefix(prefix, 0, 'prefix')
|
|
65
|
+
if (prefix === '') throw new Error('Image scaffolds must name a directory')
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Provide one safe relative directory prefix ending in /, for example website/',
|
|
69
|
+
{ cause: error },
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
const root = nextAppRoot()
|
|
73
|
+
if (root === undefined)
|
|
74
|
+
throw new Error('Run image init in a Next.js project containing app/ or src/app/')
|
|
75
|
+
const catalogArgument = relative(process.cwd(), resolve(this.receipts)).replaceAll('\\', '/')
|
|
76
|
+
if (
|
|
77
|
+
catalogArgument === '..' ||
|
|
78
|
+
catalogArgument.startsWith('../') ||
|
|
79
|
+
isAbsolute(catalogArgument)
|
|
80
|
+
)
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Catalog ${JSON.stringify(this.receipts)} is outside this Next.js app. Move it inside the app for package-import scaffolding, or use an explicit createImages factory for a shared external catalog. Nothing was written or published.`,
|
|
83
|
+
)
|
|
84
|
+
const catalog = await readStorageCatalog(this.receipts)
|
|
85
|
+
const needsCredentials =
|
|
86
|
+
this.publicDelivery || catalog === undefined || (this.writeEnv && this.privateDelivery)
|
|
87
|
+
let environment: string | undefined
|
|
88
|
+
const saved = resolveCliConfig('login')
|
|
89
|
+
if (needsCredentials && saved.loadError !== undefined) throw new Error(saved.loadError)
|
|
90
|
+
const login = saved.auth === undefined ? resolveCliConfig() : saved
|
|
91
|
+
if (needsCredentials && !this.setupClient(login)) return 1
|
|
92
|
+
if (needsCredentials) noticeCliCredentialSource(login, this.output)
|
|
93
|
+
const selectedEndpoint = this.endpoint ?? (catalog === undefined ? login.endpoint : undefined)
|
|
94
|
+
const deliveryEndpoint =
|
|
95
|
+
selectedEndpoint === undefined ||
|
|
96
|
+
new URL(selectedEndpoint).origin === 'https://api2.transloadit.com'
|
|
97
|
+
? undefined
|
|
98
|
+
: selectedEndpoint
|
|
99
|
+
if (this.writeEnv && this.privateDelivery) {
|
|
100
|
+
if (saved.credentialsLoginMethod === 'device')
|
|
101
|
+
throw new Error(
|
|
102
|
+
'Use a separate application key for private rendering. The device-login key is revoked by auth logout. Omit --write-env and configure TRANSLOADIT_KEY and TRANSLOADIT_SECRET in your application environment. Nothing was written.',
|
|
103
|
+
)
|
|
104
|
+
const value = z
|
|
105
|
+
.string()
|
|
106
|
+
.min(1)
|
|
107
|
+
.max(4096)
|
|
108
|
+
.regex(/^[^\s][^\r\n\0]*$/)
|
|
109
|
+
.refine((text) => text.trim() === text)
|
|
110
|
+
const renderingValues = z.object({ TRANSLOADIT_KEY: value, TRANSLOADIT_SECRET: value })
|
|
111
|
+
const parsed = renderingValues.safeParse({
|
|
112
|
+
TRANSLOADIT_KEY: saved.credentials?.authKey,
|
|
113
|
+
TRANSLOADIT_SECRET: saved.credentials?.authSecret,
|
|
114
|
+
})
|
|
115
|
+
if (!parsed.success)
|
|
116
|
+
throw new Error(
|
|
117
|
+
'Run transloadit auth login first to save your workspace and Auth Key. Nothing was written.',
|
|
118
|
+
)
|
|
119
|
+
// Next expands $ even in quoted dotenv values. Reject delimiters instead of silently
|
|
120
|
+
// changing an opaque secret; JSON quoting alone is not dotenv/Next escaping.
|
|
121
|
+
environment = Object.entries(parsed.data)
|
|
122
|
+
.map(([name, text]) => {
|
|
123
|
+
if (/["\\]/.test(text))
|
|
124
|
+
throw new Error(
|
|
125
|
+
'Rendering values cannot contain double quotes or backslashes; configure these values through the application environment instead',
|
|
126
|
+
)
|
|
127
|
+
return `${name}="${text.replaceAll('$', '\\$')}"\n`
|
|
128
|
+
})
|
|
129
|
+
.join('')
|
|
130
|
+
}
|
|
131
|
+
const pageDirectory = `${root}app/storage-image-example`
|
|
132
|
+
const example = this.example || this.publicDelivery
|
|
133
|
+
const files = [
|
|
134
|
+
...(example
|
|
135
|
+
? [
|
|
136
|
+
{
|
|
137
|
+
path: `${pageDirectory}/page.tsx`,
|
|
138
|
+
content: storageImagePage(
|
|
139
|
+
relative(resolve(pageDirectory), resolve(this.receipts)).replaceAll('\\', '/'),
|
|
140
|
+
prefix,
|
|
141
|
+
catalogArgument === defaultStorageCatalog ? undefined : catalogArgument,
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
: []),
|
|
146
|
+
...(this.privateDelivery
|
|
147
|
+
? [
|
|
148
|
+
{
|
|
149
|
+
path: 'transloadit.authorize.ts',
|
|
150
|
+
content: [
|
|
151
|
+
"import type { AuthorizeTransloaditImage } from '@transloadit/viewer/next/server'",
|
|
152
|
+
'',
|
|
153
|
+
'// Replace with your session and per-object authorization for workspace, template and path.',
|
|
154
|
+
'export const authorize: AuthorizeTransloaditImage = () => false',
|
|
155
|
+
'',
|
|
156
|
+
].join('\n'),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
path: `${root}app/api/storage-images/route.ts`,
|
|
160
|
+
content: "export { GET, HEAD } from '@transloadit/viewer/next/route'\n",
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
: []),
|
|
164
|
+
...(environment === undefined ? [] : [{ path: '.env.local', content: environment }]),
|
|
165
|
+
]
|
|
166
|
+
// Discover conflicts before changing server policy; exclusive creates still protect races.
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
const existing = await lstat(file.path).catch((error: unknown) => {
|
|
169
|
+
if (isErrnoException(error) && error.code === 'ENOENT') return undefined
|
|
170
|
+
throw error
|
|
171
|
+
})
|
|
172
|
+
if (existing !== undefined)
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Refusing to overwrite ${JSON.stringify(file.path)}. Move or rename it before rerunning image init; existing files were left unchanged.`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
await updateStorageReceipts(this.receipts, async (previous, signal) => {
|
|
178
|
+
// Offline example/authorizer scaffolding preserves the caller's catalog without reading
|
|
179
|
+
// remote metadata, publishing, saving credentials or choosing a different API environment.
|
|
180
|
+
if (needsCredentials || this.endpoint !== undefined)
|
|
181
|
+
assertStorageCatalogOrigin(
|
|
182
|
+
previous,
|
|
183
|
+
new URL(this.endpoint ?? login.endpoint ?? 'https://api2.transloadit.com').origin,
|
|
184
|
+
this.receipts,
|
|
185
|
+
)
|
|
186
|
+
const workspace =
|
|
187
|
+
!needsCredentials && previous !== undefined
|
|
188
|
+
? previous.workspace
|
|
189
|
+
: await resolveStorageWorkspace(this, login, previous?.workspace, signal)
|
|
190
|
+
if (this.workspace !== undefined && this.workspace !== workspace)
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Project uses ${workspace}, not ${JSON.stringify(this.workspace)}. Nothing was written.`,
|
|
193
|
+
)
|
|
194
|
+
if (previous !== undefined && previous.workspace !== workspace)
|
|
195
|
+
throw new Error(
|
|
196
|
+
'Use --receipts with a separate catalog when initializing another workspace. Nothing was written.',
|
|
197
|
+
)
|
|
198
|
+
if (this.publicDelivery) {
|
|
199
|
+
this.output.notice(
|
|
200
|
+
`Publishing ${prefix} recursively: all current and future objects under this prefix will be public.`,
|
|
201
|
+
)
|
|
202
|
+
const result = await this.client
|
|
203
|
+
.publishStoragePrefix(prefix, { signal })
|
|
204
|
+
.catch((cause: unknown) => {
|
|
205
|
+
signal.throwIfAborted()
|
|
206
|
+
throw new Error(storagePublicError(cause, workspace), { cause })
|
|
207
|
+
})
|
|
208
|
+
published = result.prefix
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
...previous,
|
|
212
|
+
apiOrigin:
|
|
213
|
+
previous?.apiOrigin ??
|
|
214
|
+
(needsCredentials || this.endpoint !== undefined
|
|
215
|
+
? new URL(this.endpoint ?? login.endpoint ?? 'https://api2.transloadit.com').origin
|
|
216
|
+
: undefined),
|
|
217
|
+
workspace,
|
|
218
|
+
// An absent delivery block means production, not permission to import another login.
|
|
219
|
+
delivery:
|
|
220
|
+
this.endpoint === undefined && previous !== undefined
|
|
221
|
+
? previous.delivery
|
|
222
|
+
: storageCatalogDelivery(selectedEndpoint),
|
|
223
|
+
public: [
|
|
224
|
+
...new Set([
|
|
225
|
+
...(previous?.public ?? []),
|
|
226
|
+
...(published === undefined ? [] : [published]),
|
|
227
|
+
]),
|
|
228
|
+
],
|
|
229
|
+
images: previous?.images ?? {},
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
// The catalog is shared Storage state: another command may update it after this lock ends.
|
|
233
|
+
// Never roll it back when unrelated scaffold-file creation fails.
|
|
234
|
+
createdCatalog = catalog === undefined
|
|
235
|
+
for (const file of files) {
|
|
236
|
+
await mkdir(dirname(file.path), { recursive: true })
|
|
237
|
+
const handle = await open(file.path, 'wx', file.path === '.env.local' ? 0o600 : 0o666)
|
|
238
|
+
created.push(file.path)
|
|
239
|
+
try {
|
|
240
|
+
await handle.writeFile(file.content)
|
|
241
|
+
} finally {
|
|
242
|
+
await handle.close()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const instruction = this.privateDelivery
|
|
246
|
+
? 'Connect your application session and per-object authorization for workspace, template and path in transloadit.authorize.ts; the generated handler denies access until then. Use a separate application key with Smart CDN on and smart_cdn:sign; assemblies:write is also accepted, but grants broader Assembly access. Configure it in Console → Credentials.'
|
|
247
|
+
: this.publicDelivery
|
|
248
|
+
? 'The directory is published. Public images use permanent unsigned CDN URLs.'
|
|
249
|
+
: 'Example created using the existing catalog; no publication policy was changed.'
|
|
250
|
+
if (deliveryEndpoint !== undefined)
|
|
251
|
+
this.output.print(
|
|
252
|
+
`Delivery uses the non-production API ${new URL(deliveryEndpoint).origin}; remove the catalog delivery block for Smart CDN delivery.`,
|
|
253
|
+
{ deliveryEndpoint: new URL(deliveryEndpoint).origin },
|
|
254
|
+
)
|
|
255
|
+
const envBlock = storageImageEnvBlock(this.publicDelivery)
|
|
256
|
+
const plugin = `Enable withTransloaditImages in next.config.ts${catalogArgument === defaultStorageCatalog ? '' : ` with { catalog: ${JSON.stringify(catalogArgument)} }`}.`
|
|
257
|
+
const filesCreated = [
|
|
258
|
+
...(createdCatalog ? [this.receipts, storageTypesPath(this.receipts)] : []),
|
|
259
|
+
...created,
|
|
260
|
+
]
|
|
261
|
+
this.output.print(
|
|
262
|
+
`Created ${filesCreated.join(', ')}\n${instruction}\n${plugin}\n${example ? `Add an image under ${prefix} with storage store and open /storage-image-example. ` : ''}Commit ${this.receipts} and transloadit-images.d.ts.\n${!this.privateDelivery ? 'Public rendering needs no environment variables, locally or on your host.' : this.writeEnv ? 'Rendering values were saved privately; never commit .env.local.' : `Add your rendering values to .env.local:\n${envBlock}`}`,
|
|
263
|
+
{ files: filesCreated, environment: envBlock },
|
|
264
|
+
)
|
|
265
|
+
return undefined
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (createdCatalog)
|
|
268
|
+
this.output.error(
|
|
269
|
+
`The catalog ${JSON.stringify(this.receipts)} and its declarations were preserved; another Storage command may already be using them.`,
|
|
270
|
+
)
|
|
271
|
+
for (const path of created) {
|
|
272
|
+
await rm(path).catch(() => {
|
|
273
|
+
this.output.error(`Could not remove partial scaffold file ${path}; remove it manually.`)
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
this.output.error(
|
|
277
|
+
`${ensureError(error).message}${published === undefined ? '' : ` The server prefix ${published} remains public; use storage unpublish deliberately if needed.`}`,
|
|
278
|
+
)
|
|
279
|
+
return 1
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -12,10 +12,25 @@ import {
|
|
|
12
12
|
AssemblyInstructionsCompileCommand,
|
|
13
13
|
RunCommand,
|
|
14
14
|
} from './assemblies.ts'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
AuthHelpCommand,
|
|
17
|
+
SignatureCommand,
|
|
18
|
+
SmartCdnSignatureCommand,
|
|
19
|
+
TokenCommand,
|
|
20
|
+
} from './auth.ts'
|
|
16
21
|
import { BillsGetCommand } from './bills.ts'
|
|
17
22
|
import { DocsRobotsGetCommand, DocsRobotsListCommand } from './docs.ts'
|
|
23
|
+
import { ImageInitCommand } from './image.ts'
|
|
24
|
+
import { AuthLoginCommand, AuthLogoutCommand, AuthStatusCommand } from './login.ts'
|
|
18
25
|
import { NotificationsReplayCommand } from './notifications.ts'
|
|
26
|
+
import {
|
|
27
|
+
StorageListCommand,
|
|
28
|
+
StoragePublicationsCommand,
|
|
29
|
+
StoragePublishCommand,
|
|
30
|
+
StorageReceiptsSyncCommand,
|
|
31
|
+
StorageStoreCommand,
|
|
32
|
+
StorageUnpublishCommand,
|
|
33
|
+
} from './storage.ts'
|
|
19
34
|
import {
|
|
20
35
|
TemplatesCreateCommand,
|
|
21
36
|
TemplatesDeleteCommand,
|
|
@@ -38,9 +53,13 @@ export function createCli(): Cli {
|
|
|
38
53
|
cli.register(Builtins.VersionCommand)
|
|
39
54
|
|
|
40
55
|
// Auth commands (signature generation)
|
|
56
|
+
cli.register(AuthHelpCommand)
|
|
41
57
|
cli.register(SignatureCommand)
|
|
42
58
|
cli.register(SmartCdnSignatureCommand)
|
|
43
59
|
cli.register(TokenCommand)
|
|
60
|
+
cli.register(AuthLoginCommand)
|
|
61
|
+
cli.register(AuthLogoutCommand)
|
|
62
|
+
cli.register(AuthStatusCommand)
|
|
44
63
|
|
|
45
64
|
// Assemblies commands
|
|
46
65
|
cli.register(AssembliesCreateCommand)
|
|
@@ -66,6 +85,13 @@ export function createCli(): Cli {
|
|
|
66
85
|
|
|
67
86
|
// Uploads commands
|
|
68
87
|
cli.register(UploadCommand)
|
|
88
|
+
cli.register(StorageStoreCommand)
|
|
89
|
+
cli.register(StorageListCommand)
|
|
90
|
+
cli.register(StoragePublicationsCommand)
|
|
91
|
+
cli.register(StoragePublishCommand)
|
|
92
|
+
cli.register(StorageUnpublishCommand)
|
|
93
|
+
cli.register(StorageReceiptsSyncCommand)
|
|
94
|
+
cli.register(ImageInitCommand)
|
|
69
95
|
|
|
70
96
|
// Prompt-to-Assembly-Instructions commands
|
|
71
97
|
cli.register(AssemblyInstructionsCompileCommand)
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { CliKeySecretCredentials } from '../helpers.ts'
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { basename, dirname } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { Command, Option } from 'clipanion'
|
|
8
|
+
import { parse } from 'dotenv'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
|
|
11
|
+
import { Transloadit } from '../../Transloadit.ts'
|
|
12
|
+
import { deviceLogin } from '../deviceLogin.ts'
|
|
13
|
+
import {
|
|
14
|
+
cliSignatureAlgorithmSchema,
|
|
15
|
+
getConfiguredCredentialsFilePath,
|
|
16
|
+
parseCliLoginSnapshot,
|
|
17
|
+
readCliInput,
|
|
18
|
+
resolveCliConfig,
|
|
19
|
+
} from '../helpers.ts'
|
|
20
|
+
import { quoteCredential } from '../secretInput.ts'
|
|
21
|
+
import { ensureError, isErrnoException } from '../types.ts'
|
|
22
|
+
import { UnauthenticatedCommand } from './BaseCommand.ts'
|
|
23
|
+
|
|
24
|
+
const credentialSchema = z
|
|
25
|
+
.string()
|
|
26
|
+
.trim()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(4096)
|
|
29
|
+
.regex(/^[^\r\n\0]+$/)
|
|
30
|
+
const credentialsSchema = z.object({
|
|
31
|
+
TRANSLOADIT_SIGNATURE_ALGORITHM: cliSignatureAlgorithmSchema.optional(),
|
|
32
|
+
TRANSLOADIT_KEY: credentialSchema,
|
|
33
|
+
TRANSLOADIT_SECRET: credentialSchema,
|
|
34
|
+
TRANSLOADIT_WORKSPACE: z
|
|
35
|
+
.string()
|
|
36
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/)
|
|
37
|
+
.optional(),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
async function existingLoginMessage(file: string): Promise<string> {
|
|
41
|
+
const metadata = await lstat(file)
|
|
42
|
+
.then(async (info) => {
|
|
43
|
+
if (!info.isFile()) return 'Saved login metadata unavailable (not a regular file).'
|
|
44
|
+
const fields = parse(await readFile(file, 'utf8'))
|
|
45
|
+
// Only identity fields are displayed; JSON quoting keeps saved terminal controls inert.
|
|
46
|
+
return [
|
|
47
|
+
`Workspace: ${fields.TRANSLOADIT_WORKSPACE ? JSON.stringify(fields.TRANSLOADIT_WORKSPACE) : 'not recorded'}`,
|
|
48
|
+
`Description: ${fields.TRANSLOADIT_AUTH_KEY_DESCRIPTION ? JSON.stringify(fields.TRANSLOADIT_AUTH_KEY_DESCRIPTION) : 'not recorded'}`,
|
|
49
|
+
`File modified: ${info.mtime.toISOString()}`,
|
|
50
|
+
].join('\n')
|
|
51
|
+
})
|
|
52
|
+
.catch(() => 'Saved login metadata unavailable (could not read the file).')
|
|
53
|
+
return [
|
|
54
|
+
`Credentials already exist at ${JSON.stringify(file)}. The saved file was preserved.`,
|
|
55
|
+
metadata,
|
|
56
|
+
'For a separate login, set TRANSLOADIT_CREDENTIALS_FILE to another file path and run transloadit auth login again.',
|
|
57
|
+
'Use --replace only if you intend to overwrite this saved login.',
|
|
58
|
+
].join('\n')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Saves CLI-only credentials without passing secrets through command-line arguments. */
|
|
62
|
+
export class AuthLoginCommand extends UnauthenticatedCommand {
|
|
63
|
+
static override paths = [['auth', 'login']]
|
|
64
|
+
static override usage = Command.Usage({
|
|
65
|
+
category: 'Authentication',
|
|
66
|
+
description: 'Log in through the browser and save your workspace Auth Key privately',
|
|
67
|
+
details:
|
|
68
|
+
'Opens a one-time browser approval; --no-browser prints the URL without opening it. --stdin accepts existing credentials and verifies a signed Template read. Honors a shell TRANSLOADIT_CREDENTIALS_FILE override, never one from project .env, and refuses app env files. Existing credentials require --replace. Uses production unless --endpoint is explicitly supplied.',
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
stdin = Option.Boolean('--stdin', false, {
|
|
72
|
+
description:
|
|
73
|
+
'Read TRANSLOADIT_KEY, TRANSLOADIT_SECRET and optional TRANSLOADIT_WORKSPACE in dotenv format; combined Smart CDN keys also need TRANSLOADIT_SIGNATURE_ALGORITHM=sha256',
|
|
74
|
+
})
|
|
75
|
+
noBrowser = Option.Boolean('--no-browser', false, {
|
|
76
|
+
description: 'Print the device approval URL without launching a browser',
|
|
77
|
+
})
|
|
78
|
+
replace = Option.Boolean('--replace', false, {
|
|
79
|
+
description: 'Explicitly replace an existing regular credentials file',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
protected async run(): Promise<number | undefined> {
|
|
83
|
+
const file = getConfiguredCredentialsFilePath('shell')
|
|
84
|
+
const temporary = `${file}.${randomUUID()}.tmp`
|
|
85
|
+
let ownsTemporary = false
|
|
86
|
+
let loginWorkspace: string | undefined
|
|
87
|
+
try {
|
|
88
|
+
if (/^\.env(?:\.|$)/i.test(basename(file)))
|
|
89
|
+
throw new Error('Credentials destination must not be an app env file')
|
|
90
|
+
const info = await lstat(file).catch((error: unknown) => {
|
|
91
|
+
if (isErrnoException(error) && error.code === 'ENOENT') return undefined
|
|
92
|
+
throw error
|
|
93
|
+
})
|
|
94
|
+
if (info !== undefined) {
|
|
95
|
+
if (!info.isFile())
|
|
96
|
+
throw new Error(
|
|
97
|
+
'Credentials must be a regular file; symlinks and directories are not replaced',
|
|
98
|
+
)
|
|
99
|
+
if (!this.replace) throw new Error(await existingLoginMessage(file))
|
|
100
|
+
}
|
|
101
|
+
// Never send newly pasted credentials to a project-controlled dotenv endpoint.
|
|
102
|
+
const endpoint = new URL(this.endpoint ?? 'https://api2.transloadit.com')
|
|
103
|
+
if (
|
|
104
|
+
(endpoint.protocol !== 'https:' &&
|
|
105
|
+
!(
|
|
106
|
+
endpoint.protocol === 'http:' &&
|
|
107
|
+
['localhost', '127.0.0.1', '[::1]'].includes(endpoint.hostname)
|
|
108
|
+
)) ||
|
|
109
|
+
endpoint.username ||
|
|
110
|
+
endpoint.password ||
|
|
111
|
+
endpoint.search ||
|
|
112
|
+
endpoint.hash ||
|
|
113
|
+
endpoint.pathname !== '/'
|
|
114
|
+
)
|
|
115
|
+
throw new Error(
|
|
116
|
+
'Login endpoint must be an HTTPS API origin (HTTP is allowed only on loopback)',
|
|
117
|
+
)
|
|
118
|
+
const origin = endpoint.origin
|
|
119
|
+
let credentials: CliKeySecretCredentials & {
|
|
120
|
+
workspace?: string
|
|
121
|
+
authKeyId?: string
|
|
122
|
+
description?: string
|
|
123
|
+
}
|
|
124
|
+
if (this.stdin) {
|
|
125
|
+
const input = credentialsSchema.safeParse(
|
|
126
|
+
parse((await readCliInput({ inputPath: '-' })).content ?? ''),
|
|
127
|
+
)
|
|
128
|
+
if (!input.success)
|
|
129
|
+
throw new Error(
|
|
130
|
+
'Provide a valid TRANSLOADIT_KEY and TRANSLOADIT_SECRET pair; nothing was saved',
|
|
131
|
+
)
|
|
132
|
+
credentials = {
|
|
133
|
+
authKey: input.data.TRANSLOADIT_KEY,
|
|
134
|
+
authSecret: input.data.TRANSLOADIT_SECRET,
|
|
135
|
+
workspace: input.data.TRANSLOADIT_WORKSPACE,
|
|
136
|
+
...(input.data.TRANSLOADIT_SIGNATURE_ALGORITHM === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: { signatureAlgorithm: input.data.TRANSLOADIT_SIGNATURE_ALGORITHM }),
|
|
139
|
+
}
|
|
140
|
+
const client = new Transloadit({
|
|
141
|
+
...credentials,
|
|
142
|
+
endpoint: origin,
|
|
143
|
+
maxRetries: 0,
|
|
144
|
+
timeout: 10_000,
|
|
145
|
+
})
|
|
146
|
+
await client.listTemplates({ pagesize: 1 }).catch((error: unknown) => {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Could not verify these credentials. Check the endpoint, key/secret and read scope at https://transloadit.com/c/${credentials.workspace ?? '<workspace>'}/template-credentials/. Combined Smart CDN keys need TRANSLOADIT_SIGNATURE_ALGORITHM=sha256 in the stdin input. Nothing was saved.`,
|
|
149
|
+
{ cause: error },
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
} else credentials = await deviceLogin(origin, this.output, this.noBrowser)
|
|
153
|
+
loginWorkspace = credentials.workspace
|
|
154
|
+
const fields = {
|
|
155
|
+
TRANSLOADIT_KEY: credentials.authKey,
|
|
156
|
+
TRANSLOADIT_SECRET: credentials.authSecret,
|
|
157
|
+
TRANSLOADIT_WORKSPACE: credentials.workspace,
|
|
158
|
+
TRANSLOADIT_WORKSPACE_VERIFIED: this.stdin ? undefined : 'true',
|
|
159
|
+
TRANSLOADIT_LOGIN_METHOD: this.stdin ? 'stdin' : 'device',
|
|
160
|
+
TRANSLOADIT_SIGNATURE_ALGORITHM: credentials.signatureAlgorithm,
|
|
161
|
+
TRANSLOADIT_ENDPOINT: this.endpoint === undefined ? undefined : origin,
|
|
162
|
+
TRANSLOADIT_AUTH_KEY_ID: credentials.authKeyId,
|
|
163
|
+
TRANSLOADIT_AUTH_KEY_DESCRIPTION: credentials.description,
|
|
164
|
+
}
|
|
165
|
+
const data = `${Object.entries(fields)
|
|
166
|
+
.flatMap(([name, value]) =>
|
|
167
|
+
value === undefined ? [] : [`${name}=${quoteCredential(value)}`],
|
|
168
|
+
)
|
|
169
|
+
.join('\n')}\n`
|
|
170
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 })
|
|
171
|
+
if (this.replace) {
|
|
172
|
+
const info = await lstat(file).catch((error: unknown) => {
|
|
173
|
+
if (isErrnoException(error) && error.code === 'ENOENT') return undefined
|
|
174
|
+
throw error
|
|
175
|
+
})
|
|
176
|
+
if (info !== undefined && !info.isFile())
|
|
177
|
+
throw new Error(
|
|
178
|
+
'Credentials must be a regular file; symlinks and directories are not replaced',
|
|
179
|
+
)
|
|
180
|
+
await writeFile(temporary, data, { flag: 'wx', mode: 0o600 })
|
|
181
|
+
ownsTemporary = true
|
|
182
|
+
await rename(temporary, file)
|
|
183
|
+
} else {
|
|
184
|
+
await writeFile(file, data, { flag: 'wx', mode: 0o600 })
|
|
185
|
+
}
|
|
186
|
+
// A signed read checks dam:write access and catalog readiness without publishing anything.
|
|
187
|
+
// It does not prove that the worker's object store can accept a later upload.
|
|
188
|
+
const storagePolicyAccess = await new Transloadit({
|
|
189
|
+
...credentials,
|
|
190
|
+
endpoint: origin,
|
|
191
|
+
maxRetries: 0,
|
|
192
|
+
timeout: 10_000,
|
|
193
|
+
})
|
|
194
|
+
.listPublicStoragePrefixes()
|
|
195
|
+
.then(
|
|
196
|
+
() => true,
|
|
197
|
+
() => false,
|
|
198
|
+
)
|
|
199
|
+
if (!storagePolicyAccess) {
|
|
200
|
+
const workspace =
|
|
201
|
+
credentials.workspace !== undefined &&
|
|
202
|
+
/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(credentials.workspace)
|
|
203
|
+
? credentials.workspace
|
|
204
|
+
: '<workspace>'
|
|
205
|
+
this.output.warn(
|
|
206
|
+
`Login saved, but Storage policy access could not be verified. Before uploading, check Storage availability and the Auth Key dam:write scope at https://transloadit.com/c/${workspace}/template-credentials/. A temporary network failure can also prevent this check; retry with transloadit storage publications.`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
const message =
|
|
210
|
+
credentials.workspace === undefined
|
|
211
|
+
? `Verified one signed Template read and saved CLI credentials to ${file}. Application env files were not changed.`
|
|
212
|
+
: `Logged in to workspace ${credentials.workspace}`
|
|
213
|
+
this.output.print(
|
|
214
|
+
`${message}${storagePolicyAccess ? '\nStorage policy access verified.' : ''}`,
|
|
215
|
+
{
|
|
216
|
+
saved: file,
|
|
217
|
+
storagePolicyAccess,
|
|
218
|
+
verified: true,
|
|
219
|
+
...(credentials.workspace === undefined ? {} : { workspace: credentials.workspace }),
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
return undefined
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.output.error(
|
|
225
|
+
isErrnoException(error) && error.code === 'EEXIST'
|
|
226
|
+
? [
|
|
227
|
+
await existingLoginMessage(file),
|
|
228
|
+
`This login’s ${this.stdin ? 'verified' : 'approved'} Auth Key was not saved.`,
|
|
229
|
+
this.stdin
|
|
230
|
+
? 'No new Auth Key was created. The supplied key may still be used by other applications.'
|
|
231
|
+
: `Review it in Console → Credentials: https://transloadit.com/c/${loginWorkspace ?? '<workspace>'}/template-credentials/; revoke it there if no longer needed.`,
|
|
232
|
+
'Be careful: auth logout would use the saved login, not this unsaved one.',
|
|
233
|
+
].join('\n')
|
|
234
|
+
: ensureError(error).message,
|
|
235
|
+
)
|
|
236
|
+
return 1
|
|
237
|
+
} finally {
|
|
238
|
+
if (ownsTemporary) await rm(temporary, { force: true })
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Shows the saved CLI login, never a signing key or secret. */
|
|
244
|
+
export class AuthStatusCommand extends UnauthenticatedCommand {
|
|
245
|
+
static override paths = [['auth', 'status']]
|
|
246
|
+
static override usage = Command.Usage({
|
|
247
|
+
category: 'Authentication',
|
|
248
|
+
description: 'Show the saved CLI workspace and key description',
|
|
249
|
+
})
|
|
250
|
+
protected run(): Promise<number | undefined> {
|
|
251
|
+
const config = resolveCliConfig('login')
|
|
252
|
+
if (config.credentials === undefined) {
|
|
253
|
+
this.output.error(config.loadError ?? 'Not logged in. Run transloadit auth login.')
|
|
254
|
+
return Promise.resolve(1)
|
|
255
|
+
}
|
|
256
|
+
const workspace = config.credentialsWorkspace ?? 'not recorded'
|
|
257
|
+
const description =
|
|
258
|
+
config.credentialsDescription ?? 'Existing Auth Key (description not recorded)'
|
|
259
|
+
this.output.print(
|
|
260
|
+
`Saved login: ${workspace}\n${description}\nStorage commands report any shell or project credential override before use.`,
|
|
261
|
+
{ workspace, description },
|
|
262
|
+
)
|
|
263
|
+
return Promise.resolve(undefined)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Revokes browser-login keys; imported application keys need explicit revocation consent. */
|
|
268
|
+
export class AuthLogoutCommand extends UnauthenticatedCommand {
|
|
269
|
+
static override paths = [['auth', 'logout']]
|
|
270
|
+
static override usage = Command.Usage({
|
|
271
|
+
category: 'Authentication',
|
|
272
|
+
description: 'Remove saved credentials; browser-login keys are also revoked',
|
|
273
|
+
})
|
|
274
|
+
revoke = Option.Boolean('--revoke', {
|
|
275
|
+
description: 'Also revoke an imported Auth Key; other applications using it will stop working',
|
|
276
|
+
})
|
|
277
|
+
protected async run(): Promise<number | undefined> {
|
|
278
|
+
try {
|
|
279
|
+
if (this.revoke === false)
|
|
280
|
+
throw new Error(
|
|
281
|
+
'--no-revoke is not supported. Nothing was changed; browser-login logout revokes its key.',
|
|
282
|
+
)
|
|
283
|
+
const file = getConfiguredCredentialsFilePath('shell')
|
|
284
|
+
if (/^\.env(?:\.|$)/i.test(basename(file)))
|
|
285
|
+
throw new Error('Logout never removes application env files')
|
|
286
|
+
const info = await lstat(file)
|
|
287
|
+
if (!info.isFile())
|
|
288
|
+
throw new Error('Logout requires a regular credentials file, not a symlink or directory')
|
|
289
|
+
const before = await readFile(file, 'utf8')
|
|
290
|
+
// Imported and legacy keys may be shared with applications; never infer disposability.
|
|
291
|
+
const revoke = this.revoke === true || parse(before).TRANSLOADIT_LOGIN_METHOD === 'device'
|
|
292
|
+
if (revoke) {
|
|
293
|
+
const config = parseCliLoginSnapshot(before)
|
|
294
|
+
if (config.credentials === undefined) throw new Error(config.loadError ?? 'Not logged in')
|
|
295
|
+
const endpoint = config.credentialsEndpoint ?? 'https://api2.transloadit.com'
|
|
296
|
+
if (
|
|
297
|
+
this.endpoint !== undefined &&
|
|
298
|
+
new URL(this.endpoint).origin !== new URL(endpoint).origin
|
|
299
|
+
)
|
|
300
|
+
throw new Error('Logout must use the saved login endpoint; no credentials were sent')
|
|
301
|
+
await new Transloadit({ ...config.credentials, endpoint, maxRetries: 0, timeout: 10_000 })
|
|
302
|
+
.revokeOwnAuthKey()
|
|
303
|
+
.catch((cause: unknown) => {
|
|
304
|
+
throw new Error(
|
|
305
|
+
'The CLI key was not revoked. Check connectivity and Console key permissions, then retry; the credentials file was kept.',
|
|
306
|
+
{ cause },
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
if (!(await lstat(file)).isFile() || (await readFile(file, 'utf8')) !== before)
|
|
311
|
+
throw new Error(
|
|
312
|
+
`${revoke ? 'The key was revoked, but the' : 'The'} credentials file changed during logout and was preserved`,
|
|
313
|
+
)
|
|
314
|
+
await rm(file)
|
|
315
|
+
this.output.print(
|
|
316
|
+
revoke
|
|
317
|
+
? 'CLI key revoked and saved credentials removed.'
|
|
318
|
+
: 'Saved credentials removed. Remote credentials were not revoked.',
|
|
319
|
+
{
|
|
320
|
+
revoked: revoke,
|
|
321
|
+
removed: true,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
return undefined
|
|
325
|
+
} catch (error) {
|
|
326
|
+
this.output.error(
|
|
327
|
+
isErrnoException(error) && error.code === 'ENOENT'
|
|
328
|
+
? 'Not logged in; no saved credentials file.'
|
|
329
|
+
: ensureError(error).message,
|
|
330
|
+
)
|
|
331
|
+
return 1
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|