@capgo/cli 5.0.0-alpha.3 → 5.0.0-alpha.7

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.
@@ -8,6 +8,10 @@ import * as p from '@clack/prompts'
8
8
  import { checksum as getChecksum } from '@tomasklaen/checksum'
9
9
  import ciDetect from 'ci-info'
10
10
  import ky from 'ky'
11
+ import {
12
+ PutObjectCommand,
13
+ S3Client,
14
+ } from '@aws-sdk/client-s3'
11
15
  import { checkLatest } from '../api/update'
12
16
  import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
13
17
  import { encryptSource } from '../api/crypto'
@@ -15,6 +19,7 @@ import type {
15
19
  OptionsBase,
16
20
  } from '../utils'
17
21
  import {
22
+ EMPTY_UUID,
18
23
  OrganizationPerm,
19
24
  baseKey,
20
25
  checKOldEncryption,
@@ -28,6 +33,7 @@ import {
28
33
  getConfig,
29
34
  getLocalConfig,
30
35
  getLocalDepenencies,
36
+ getOrganizationId,
31
37
  hasOrganizationPerm,
32
38
  regexSemver,
33
39
  requireUpdateMetadata,
@@ -50,6 +56,10 @@ interface Options extends OptionsBase {
50
56
  key?: boolean | string
51
57
  keyData?: string
52
58
  ivSessionKey?: string
59
+ s3Region?: string
60
+ s3Apikey?: string
61
+ s3Apisecret?: string
62
+ s3BucketName?: string
53
63
  bundleUrl?: boolean
54
64
  codeCheck?: boolean
55
65
  minUpdateVersion?: string
@@ -63,6 +73,21 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
63
73
  let { bundle, path, channel } = options
64
74
  const { external, key, displayIvSession, autoMinUpdateVersion, ignoreMetadataCheck } = options
65
75
  let { minUpdateVersion } = options
76
+ const { s3Region, s3Apikey, s3Apisecret, s3BucketName } = options
77
+ let useS3 = false
78
+ let s3Client
79
+ if (s3Region && s3Apikey && s3Apisecret && s3BucketName) {
80
+ p.log.info('Uploading to S3')
81
+ useS3 = true
82
+ s3Client = new S3Client({
83
+ region: s3Region,
84
+ credentials: {
85
+ accessKeyId: s3Apikey,
86
+ secretAccessKey: s3Apisecret,
87
+ },
88
+ })
89
+ }
90
+
66
91
  options.apikey = options.apikey || findSavedKey()
67
92
  const snag = useLogSnag()
68
93
 
@@ -91,6 +116,13 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
91
116
  p.log.error('Missing argument, you need to provide a appid and a bundle and a path, or be in a capacitor project')
92
117
  program.error('')
93
118
  }
119
+ // if one S3 variable is set, check that all are set
120
+ if (s3BucketName || s3Region || s3Apikey || s3Apisecret) {
121
+ if (!s3BucketName || !s3Region || !s3Apikey || !s3Apisecret) {
122
+ p.log.error('Missing argument, for S3 upload you need to provide a bucket name, region, API key, and API secret')
123
+ program.error('')
124
+ }
125
+ }
94
126
  // check if path exist
95
127
  if (!existsSync(path)) {
96
128
  p.log.error(`Path ${path} does not exist, build your app first, or provide a valid path`)
@@ -119,9 +151,12 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
119
151
  // await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appid);
120
152
 
121
153
  const permissions = await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appid, OrganizationPerm.upload)
122
- await checkPlanValid(supabase, userId, options.apikey, appid, true)
123
154
 
124
- const updateMetadataRequired = await requireUpdateMetadata(supabase, channel)
155
+ // Now if it does exist we will fetch the org id
156
+ const orgId = await getOrganizationId(supabase, appid)
157
+ await checkPlanValid(supabase, orgId, options.apikey, appid, true)
158
+
159
+ const updateMetadataRequired = await requireUpdateMetadata(supabase, channel, appid)
125
160
 
126
161
  // Check compatibility here
127
162
  const { data: channelData, error: channelError } = await supabase
@@ -197,11 +232,12 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
197
232
  }
198
233
 
199
234
  const { data: isTrial, error: isTrialsError } = await supabase
200
- .rpc('is_trial', { userid: userId })
235
+ .rpc('is_trial_org', { orgid: orgId })
201
236
  .single()
202
237
  if ((isTrial && isTrial > 0) || isTrialsError) {
238
+ // TODO: Come back to this to fix for orgs v3
203
239
  p.log.warn(`WARNING !!\nTrial expires in ${isTrial} days`)
204
- p.log.warn(`Upgrade here: ${localConfig.hostWeb}/dashboard/settings/plans`)
240
+ p.log.warn(`Upgrade here: ${localConfig.hostWeb}/dashboard/settings/plans?oid=${orgId}`)
205
241
  }
206
242
 
207
243
  // check if app already exist
@@ -213,14 +249,11 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
213
249
  p.log.error(`Version already exists ${formatError(appVersionError)}`)
214
250
  program.error('')
215
251
  }
216
- // make bundle safe for s3 name https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
217
- const safeBundle = bundle.replace(/[^a-zA-Z0-9-_.!*'()]/g, '__')
218
- const fileName = `${safeBundle}.zip`
219
252
 
220
253
  let sessionKey
221
254
  let checksum = ''
222
255
  let zipped: Buffer | null = null
223
- if (!external) {
256
+ if (!external && useS3 === false) {
224
257
  const zip = new AdmZip()
225
258
  zip.addLocalFolder(path)
226
259
  zipped = zip.toBuffer()
@@ -241,7 +274,7 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
241
274
  if (ciDetect.isCI)
242
275
  program.error('')
243
276
 
244
- const res = await p.confirm({ message: 'Do you want to use our public key ?' })
277
+ const res = await p.confirm({ message: 'Do you want to use our private key ?' })
245
278
  if (!res) {
246
279
  p.log.error(`Error: Missing public key`)
247
280
  program.error('')
@@ -264,6 +297,10 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
264
297
  keyData = keyFile.toString()
265
298
  }
266
299
  // encrypt
300
+ if (keyData && !keyData.startsWith('-----BEGIN RSA PRIVATE KEY-----')) {
301
+ p.log.error(`the private key provided is not a valid RSA Private key`)
302
+ program.error('')
303
+ }
267
304
  p.log.info(`Encrypting your bundle`)
268
305
  const res = encryptSource(zipped, keyData)
269
306
  sessionKey = res.ivSessionKey
@@ -295,6 +332,15 @@ It will be also visible in your dashboard\n`)
295
332
  program.error('')
296
333
  }
297
334
  else {
335
+ if (useS3) {
336
+ const zip = new AdmZip()
337
+ zip.addLocalFolder(path)
338
+ zipped = zip.toBuffer()
339
+ const s = p.spinner()
340
+ s.start(`Calculating checksum`)
341
+ checksum = await getChecksum(zipped, 'crc32')
342
+ s.stop(`Checksum: ${checksum}`)
343
+ }
298
344
  await snag.track({
299
345
  channel: 'app',
300
346
  event: 'App external',
@@ -319,8 +365,7 @@ It will be also visible in your dashboard\n`)
319
365
  const appOwner = await getAppOwner(supabase, appid)
320
366
 
321
367
  const versionData = {
322
- bucket_id: external ? undefined : fileName,
323
- user_id: appOwner,
368
+ // bucket_id: external ? undefined : fileName,
324
369
  name: bundle,
325
370
  app_id: appid,
326
371
  session_key: sessionKey,
@@ -328,6 +373,8 @@ It will be also visible in your dashboard\n`)
328
373
  storage_provider: external ? 'external' : 'r2-direct',
329
374
  minUpdateVersion,
330
375
  native_packages: nativePackages,
376
+ owner_org: EMPTY_UUID,
377
+ user_id: userId,
331
378
  checksum,
332
379
  }
333
380
  const { error: dbError } = await updateOrCreateVersion(supabase, versionData)
@@ -339,22 +386,29 @@ It will be also visible in your dashboard\n`)
339
386
  const spinner = p.spinner()
340
387
  spinner.start(`Uploading Bundle`)
341
388
 
342
- const url = await uploadUrl(supabase, appid, fileName)
389
+ const url = await uploadUrl(supabase, appid, bundle)
343
390
  if (!url) {
344
391
  p.log.error(`Cannot get upload url`)
345
392
  program.error('')
346
393
  }
347
- await ky.put(url, {
348
- timeout: 60000,
349
- body: zipped,
350
- headers: (!localS3
351
- ? {
352
- 'Content-Type': 'application/octet-stream',
353
- 'Cache-Control': 'public, max-age=456789, immutable',
354
- 'x-amz-meta-crc32': checksum,
355
- }
356
- : undefined),
357
- })
394
+ try {
395
+ await ky.put(url, {
396
+ timeout: 60000,
397
+ retry: 5,
398
+ body: zipped,
399
+ headers: (!localS3
400
+ ? {
401
+ 'Content-Type': 'application/octet-stream',
402
+ 'Cache-Control': 'public, max-age=456789, immutable',
403
+ 'x-amz-meta-crc32': checksum,
404
+ }
405
+ : undefined),
406
+ })
407
+ }
408
+ catch (errorUpload) {
409
+ p.log.error(`Cannot upload bundle ${formatError(errorUpload)}`)
410
+ program.error('')
411
+ }
358
412
  versionData.storage_provider = 'r2'
359
413
  const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
360
414
  if (dbError2) {
@@ -363,6 +417,33 @@ It will be also visible in your dashboard\n`)
363
417
  }
364
418
  spinner.stop('Bundle Uploaded 💪')
365
419
  }
420
+ else if (useS3 && zipped) {
421
+ const spinner = p.spinner()
422
+ spinner.start(`Uploading Bundle`)
423
+
424
+ const fileName = `${appid}-${bundle}`
425
+ const encodeFileName = encodeURIComponent(fileName)
426
+ const command: PutObjectCommand = new PutObjectCommand({
427
+ Bucket: s3BucketName,
428
+ Key: fileName,
429
+ Body: zipped,
430
+ })
431
+
432
+ const response = await s3Client.send(command)
433
+ if (response.$metadata.httpStatusCode !== 200) {
434
+ p.log.error(`Cannot upload to S3`)
435
+ program.error('')
436
+ }
437
+
438
+ versionData.storage_provider = 'external'
439
+ versionData.external_url = `https://${s3BucketName}.s3.amazonaws.com/${encodeFileName}`
440
+ const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
441
+ if (dbError2) {
442
+ p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
443
+ program.error('')
444
+ }
445
+ spinner.stop('Bundle Uploaded 💪')
446
+ }
366
447
  const { data: versionId } = await supabase
367
448
  .rpc('get_app_versions', { apikey: options.apikey, name_version: bundle, appid })
368
449
  .single()
@@ -373,6 +454,7 @@ It will be also visible in your dashboard\n`)
373
454
  app_id: appid,
374
455
  created_by: appOwner,
375
456
  version: versionId,
457
+ owner_org: EMPTY_UUID,
376
458
  })
377
459
  if (dbError3) {
378
460
  p.log.error(`Cannot set channel, the upload key is not allowed to do that, use the "all" for this. ${formatError(dbError3)}`)
package/src/bundle/zip.ts CHANGED
@@ -112,15 +112,6 @@ export async function zipBundle(appId: string, options: Options) {
112
112
  if (!json)
113
113
  s2.stop(`Saved to ${name}`)
114
114
 
115
- if (options.json) {
116
- const output = {
117
- bundle,
118
- filename: name,
119
- checksum,
120
- }
121
- p.log.info(formatError(output))
122
- }
123
-
124
115
  await snag.track({
125
116
  channel: 'app',
126
117
  event: 'App zip',
@@ -133,5 +124,16 @@ export async function zipBundle(appId: string, options: Options) {
133
124
 
134
125
  if (!json)
135
126
  p.outro(`Done ✅`)
127
+
128
+ if (json) {
129
+ const output = {
130
+ bundle,
131
+ filename: name,
132
+ checksum,
133
+ }
134
+ // Keep the console log and stringify for user who parse the output
135
+ // eslint-disable-next-line no-console
136
+ console.log(JSON.stringify(output, null, 2))
137
+ }
136
138
  process.exit()
137
139
  }
@@ -1,10 +1,10 @@
1
1
  import process from 'node:process'
2
2
  import { program } from 'commander'
3
3
  import * as p from '@clack/prompts'
4
- import { checkAppExistsAndHasPermissionErr } from '../api/app'
4
+ import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
5
5
  import { createChannel, findUnknownVersion } from '../api/channels'
6
6
  import type { OptionsBase } from '../utils'
7
- import { createSupabaseClient, findSavedKey, getConfig, useLogSnag, verifyUser } from '../utils'
7
+ import { EMPTY_UUID, OrganizationPerm, createSupabaseClient, findSavedKey, getConfig, useLogSnag, verifyUser } from '../utils'
8
8
 
9
9
  interface Options extends OptionsBase {
10
10
  default?: boolean
@@ -29,7 +29,7 @@ export async function addChannel(channelId: string, appId: string, options: Opti
29
29
 
30
30
  const userId = await verifyUser(supabase, options.apikey, ['write', 'all'])
31
31
  // Check we have app access to this appId
32
- await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appId)
32
+ await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.admin)
33
33
 
34
34
  p.log.info(`Creating channel ${appId}#${channelId} to Capgo`)
35
35
  try {
@@ -42,7 +42,7 @@ export async function addChannel(channelId: string, appId: string, options: Opti
42
42
  name: channelId,
43
43
  app_id: appId,
44
44
  version: data.id,
45
- created_by: userId,
45
+ owner_org: EMPTY_UUID,
46
46
  })
47
47
  p.log.success(`Channel created ✅`)
48
48
  await snag.track({
@@ -1,9 +1,9 @@
1
1
  import process from 'node:process'
2
2
  import { program } from 'commander'
3
3
  import * as p from '@clack/prompts'
4
- import { checkAppExistsAndHasPermissionErr } from '../api/app'
4
+ import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
5
5
  import type { OptionsBase } from '../utils'
6
- import { createSupabaseClient, findSavedKey, getConfig, verifyUser } from '../utils'
6
+ import { OrganizationPerm, createSupabaseClient, findSavedKey, getConfig, verifyUser } from '../utils'
7
7
 
8
8
  interface Options extends OptionsBase {
9
9
  channel?: string
@@ -36,9 +36,9 @@ export async function currentBundle(channel: string, appId: string, options: Opt
36
36
  }
37
37
  const supabase = await createSupabaseClient(options.apikey)
38
38
 
39
- const userId = await verifyUser(supabase, options.apikey, ['write', 'all', 'read'])
39
+ const _userId = await verifyUser(supabase, options.apikey, ['write', 'all', 'read'])
40
40
  // Check we have app access to this appId
41
- await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appId)
41
+ await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.read)
42
42
 
43
43
  if (!channel) {
44
44
  p.log.error(`Please provide a channel to get the bundle from.`)
@@ -50,7 +50,6 @@ export async function currentBundle(channel: string, appId: string, options: Opt
50
50
  .select('version ( name )')
51
51
  .eq('name', channel)
52
52
  .eq('app_id', appId)
53
- .eq('created_by', userId)
54
53
  .limit(1)
55
54
 
56
55
  if (error || supabaseChannel.length === 0) {
@@ -1,10 +1,10 @@
1
1
  import process from 'node:process'
2
2
  import { program } from 'commander'
3
3
  import * as p from '@clack/prompts'
4
- import { checkAppExistsAndHasPermissionErr } from '../api/app'
4
+ import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
5
5
  import { delChannel } from '../api/channels'
6
6
  import type { OptionsBase } from '../utils'
7
- import { createSupabaseClient, findSavedKey, getConfig, useLogSnag, verifyUser } from '../utils'
7
+ import { OrganizationPerm, createSupabaseClient, findSavedKey, getConfig, useLogSnag, verifyUser } from '../utils'
8
8
 
9
9
  export async function deleteChannel(channelId: string, appId: string, options: OptionsBase) {
10
10
  p.intro(`Delete channel`)
@@ -25,7 +25,7 @@ export async function deleteChannel(channelId: string, appId: string, options: O
25
25
 
26
26
  const userId = await verifyUser(supabase, options.apikey, ['write', 'all'])
27
27
  // Check we have app access to this appId
28
- await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appId)
28
+ await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.admin)
29
29
 
30
30
  p.log.info(`Deleting channel ${appId}#${channelId} from Capgo`)
31
31
  try {
@@ -1,10 +1,10 @@
1
1
  import process from 'node:process'
2
2
  import { program } from 'commander'
3
3
  import * as p from '@clack/prompts'
4
- import { checkAppExistsAndHasPermissionErr } from '../api/app'
4
+ import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
5
5
  import { displayChannels, getActiveChannels } from '../api/channels'
6
6
  import type { OptionsBase } from '../utils'
7
- import { createSupabaseClient, findSavedKey, getConfig, useLogSnag, verifyUser } from '../utils'
7
+ import { OrganizationPerm, createSupabaseClient, findSavedKey, getConfig, useLogSnag, verifyUser } from '../utils'
8
8
 
9
9
  export async function listChannels(appId: string, options: OptionsBase) {
10
10
  p.intro(`List channels`)
@@ -24,7 +24,7 @@ export async function listChannels(appId: string, options: OptionsBase) {
24
24
 
25
25
  const userId = await verifyUser(supabase, options.apikey, ['write', 'all', 'read', 'upload'])
26
26
  // Check we have app access to this appId
27
- await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appId)
27
+ await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.read)
28
28
 
29
29
  p.log.info(`Querying available channels in Capgo`)
30
30
 
@@ -2,16 +2,18 @@ import process from 'node:process'
2
2
  import { program } from 'commander'
3
3
  import * as p from '@clack/prompts'
4
4
  import type { Database } from '../types/supabase.types'
5
- import { checkAppExistsAndHasPermissionErr } from '../api/app'
5
+ import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
6
6
  import type {
7
7
  OptionsBase,
8
8
  } from '../utils'
9
9
  import {
10
+ OrganizationPerm,
10
11
  checkPlanValid,
11
12
  createSupabaseClient,
12
13
  findSavedKey,
13
14
  formatError,
14
15
  getConfig,
16
+ getOrganizationId,
15
17
  updateOrCreateChannel,
16
18
  useLogSnag,
17
19
  verifyUser,
@@ -30,7 +32,7 @@ interface Options extends OptionsBase {
30
32
  channel?: string
31
33
  }
32
34
 
33
- const disableAutoUpdatesPossibleOptions = ['major', 'minor', 'metadata', 'none']
35
+ const disableAutoUpdatesPossibleOptions = ['major', 'minor', 'metadata', 'patch', 'none']
34
36
 
35
37
  export async function setChannel(channel: string, appId: string, options: Options) {
36
38
  p.intro(`Set channel`)
@@ -51,7 +53,8 @@ export async function setChannel(channel: string, appId: string, options: Option
51
53
 
52
54
  const userId = await verifyUser(supabase, options.apikey, ['write', 'all'])
53
55
  // Check we have app access to this appId
54
- await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appId)
56
+ await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.admin)
57
+ const orgId = await getOrganizationId(supabase, appId)
55
58
 
56
59
  const { bundle, latest, downgrade, upgrade, ios, android, selfAssign, state, disableAutoUpdate } = options
57
60
  if (!channel) {
@@ -75,7 +78,7 @@ export async function setChannel(channel: string, appId: string, options: Option
75
78
  program.error('')
76
79
  }
77
80
  try {
78
- await checkPlanValid(supabase, userId, options.apikey, appId)
81
+ await checkPlanValid(supabase, orgId, options.apikey, appId)
79
82
  const channelPayload: Database['public']['Tables']['channels']['Insert'] = {
80
83
  created_by: userId,
81
84
  app_id: appId,
package/src/index.ts CHANGED
@@ -113,6 +113,10 @@ bundle
113
113
  .option('-c, --channel <channel>', 'channel to link to')
114
114
  .option('-e, --external <url>', 'link to external url intead of upload to Capgo Cloud')
115
115
  .option('--iv-session-key <key>', 'Set the iv and session key for bundle url external')
116
+ .option('--s3-region <region>', 'Region for your AWS S3 bucket')
117
+ .option('--s3-apikey <apikey>', 'apikey for your AWS S3 account')
118
+ .option('--s3-apisecret <apisecret>', 'api secret for your AWS S3 account')
119
+ .option('--s3-bucket-name <bucketName>', 'Name for your AWS S3 bucket')
116
120
  .option('--key <key>', 'custom path for public signing key')
117
121
  .option('--key-data <keyData>', 'base64 public signing key')
118
122
  .option('--bundle-url', 'prints bundle url into stdout')
@@ -148,12 +152,13 @@ bundle
148
152
  .action(listBundle)
149
153
  .option('-a, --apikey <apikey>', 'apikey to link to your account')
150
154
 
151
- bundle
152
- .command('unlink [appId]')
153
- .description('Unlink a bundle in Capgo Cloud')
154
- .action(listBundle)
155
- .option('-a, --apikey <apikey>', 'apikey to link to your account')
156
- .option('-b, --bundle <bundle>', 'bundle version number of the bundle to unlink')
155
+ // TODO: Fix this command!
156
+ // bundle
157
+ // .command('unlink [appId]')
158
+ // .description('Unlink a bundle in Capgo Cloud')
159
+ // .action(listBundle)
160
+ // .option('-a, --apikey <apikey>', 'apikey to link to your account')
161
+ // .option('-b, --bundle <bundle>', 'bundle version number of the bundle to unlink')
157
162
 
158
163
  bundle
159
164
  .command('cleanup [appId]')
@@ -242,7 +247,7 @@ channel
242
247
  .option('--no-android', 'Disable sending update to android devices')
243
248
  .option('--self-assign', 'Allow to device to self assign to this channel')
244
249
  .option('--no-self-assign', 'Disable devices to self assign to this channel')
245
- .option('--disable-auto-update <disableAutoUpdate>', 'Disable auto update strategy for this channel.The possible options are: major, minor, metadata, none')
250
+ .option('--disable-auto-update <disableAutoUpdate>', 'Disable auto update strategy for this channel.The possible options are: major, minor, metadata, patch, none')
246
251
 
247
252
  const key = program
248
253
  .command('key')