@shoplifter/cli 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Shoplifter CLI
2
2
 
3
- Requires Node 24. Install with `npm install --global @shoplifter/cli@0.1.0`.
4
- The public SDK, contract and CLI release versions match.
3
+ Requires Node 24. Install with `npm install --global @shoplifter/cli@0.1.1`.
4
+ It continues using SDK and contract `0.1.0`; CLI patches do not upgrade runtime inputs.
5
5
 
6
6
  ## Authoring
7
7
 
@@ -38,14 +38,22 @@ Set `SHOPLIFTER_API_URL=https://<platform-api-host>` before production operation
38
38
  runtime, verifies hashes and explicitly updates local archives and the lockfile.
39
39
  It preserves authored files. Run `npm ci --ignore-scripts` after synchronization.
40
40
 
41
- `shoplifter login` opens dashboard approval for a specific channel. One-hour credentials
41
+ `shoplifter login combo-media` opens dashboard approval for that channel code.
42
+ With `shoplifter login` alone, explicitly choose a channel on the approval page.
43
+ Only channels accessible to your dashboard account can be selected; publishing must
44
+ be enabled for the channel. Switching channels clears the reviewed request.
45
+ One-hour credentials
42
46
  are stored with private permissions outside the project. Use `shoplifter logout` to revoke.
43
- `shoplifter publication upload <channel-id>` uploads source; `stage`, `preview`,
47
+ `shoplifter publication upload <channel-code>` uploads source; `stage`, `preview`,
44
48
  `build`, `activate`, `publish`, `status`, `logs`, `retry`, `retry-deployment` and
45
49
  `rollback` operate on channel-scoped revisions. `preview-link` requests protected
46
50
  preview access. Source changes require exact-input preview and explicit activation.
47
51
  Downloading the SDK never grants publication permission. The loopback bypass is
48
52
  development-only and cannot be used remotely or in production.
53
+ Older credentials resolve their channel code through the authenticated session API.
54
+ Set `SHOPLIFTER_NO_BROWSER=1` to copy the approval link without opening a browser.
55
+ Deploy the matching backend/dashboard changes before releasing CLI `0.1.1`;
56
+ this patch requires the authenticated session endpoint and code-based operations.
49
57
 
50
58
  Shoplifter-owned code is Apache-2.0; separately installed dependencies retain their
51
59
  licenses. See `LICENSE`, `NOTICE` and `THIRD_PARTY_NOTICES`. This does not license
package/dist/auth.d.ts CHANGED
@@ -2,8 +2,10 @@ export declare function apiOrigin(): URL;
2
2
  export declare function credentials(required?: boolean): Promise<{
3
3
  token: string;
4
4
  channelId: string;
5
+ channelCode?: string;
5
6
  expiresAt: string;
6
7
  } | undefined>;
7
- export declare function login(): Promise<void>;
8
+ export declare function sessionChannel(token: string): Promise<string>;
9
+ export declare function login(channelCode?: string): Promise<void>;
8
10
  export declare function logout(): Promise<void>;
9
11
  export declare function syncDependencies(): Promise<void>;
package/dist/auth.js CHANGED
@@ -33,15 +33,25 @@ export async function credentials(required = true) {
33
33
  }
34
34
  async function response(value) { if (!value.ok)
35
35
  throw new Error(`CLI request failed (${value.status})`); return value.json(); }
36
- export async function login() {
36
+ export async function sessionChannel(token) {
37
+ const result = await response(await fetch(new URL('/shoplifter-cli/session', apiOrigin()), { headers: { authorization: 'Bearer ' + token }, signal: AbortSignal.timeout(15000) }));
38
+ if (typeof result.channelCode !== 'string' || !result.channelCode.trim())
39
+ throw new Error('Invalid CLI channel response');
40
+ return result.channelCode;
41
+ }
42
+ export async function login(channelCode) {
43
+ if (channelCode !== undefined && (!channelCode.trim() || channelCode !== channelCode.trim() || channelCode.length > 255))
44
+ throw new Error('A channel code is required');
37
45
  const origin = apiOrigin();
38
46
  const grant = await response(await fetch(new URL('/shoplifter-cli/device', origin), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ scopes: ['read', 'content', 'source'] }), signal: AbortSignal.timeout(15000) }));
39
47
  const url = new URL(grant.verificationUri);
40
48
  if (url.origin !== origin.origin || !/^[A-F0-9]{10}$/.test(grant.userCode))
41
49
  throw new Error('Invalid CLI approval response');
42
50
  url.searchParams.set('code', grant.userCode);
51
+ if (channelCode)
52
+ url.searchParams.set('channel', channelCode);
43
53
  console.log(`Approve code ${grant.userCode} in your dashboard: ${url}`);
44
- if (['darwin', 'linux'].includes(process.platform)) {
54
+ if (process.env.SHOPLIFTER_NO_BROWSER !== '1' && ['darwin', 'linux'].includes(process.platform)) {
45
55
  const child = spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url.href], { stdio: 'ignore', detached: true });
46
56
  child.on('error', () => undefined);
47
57
  child.unref();
@@ -57,12 +67,17 @@ export async function login() {
57
67
  || !Number.isFinite(Date.parse(result.expiresAt)) || Date.parse(result.expiresAt) <= Date.now()
58
68
  || Date.parse(result.expiresAt) > Date.now() + 3600000)
59
69
  throw new Error('Invalid CLI credential response');
70
+ const approvedCode = await sessionChannel(result.token);
71
+ if (channelCode && approvedCode !== channelCode) {
72
+ await response(await fetch(new URL('/shoplifter-cli/logout', origin), { method: 'POST', headers: { authorization: 'Bearer ' + result.token }, signal: AbortSignal.timeout(15000) }));
73
+ throw new Error('A different channel was approved; run shoplifter login with the intended channel code');
74
+ }
60
75
  await mkdir(directory(), { recursive: true, mode: 0o700 });
61
76
  await chmod(directory(), 0o700);
62
77
  const temporary = filename() + '.' + randomUUID();
63
- await writeFile(temporary, JSON.stringify({ token: result.token, channelId: result.channelId, expiresAt: result.expiresAt }), { mode: 0o600, flag: 'wx' });
78
+ await writeFile(temporary, JSON.stringify({ token: result.token, channelId: result.channelId, channelCode: approvedCode, expiresAt: result.expiresAt }), { mode: 0o600, flag: 'wx' });
64
79
  await rename(temporary, filename());
65
- console.log(`Authorized channel ${result.channelId} until ${result.expiresAt}`);
80
+ console.log(`Authorized channel ${approvedCode} until ${result.expiresAt}`);
66
81
  return;
67
82
  }
68
83
  throw new Error('CLI approval timed out');
package/dist/index.js CHANGED
@@ -17,9 +17,19 @@ import { initialize, editProject, scaffold } from './author.js';
17
17
  import { generate, validateImages } from './files.js';
18
18
  import { publication, previewUpload } from './publication.js';
19
19
  import { login, logout, syncDependencies } from './auth.js';
20
+ // commander-ts exits on async command failures without reporting the error.
21
+ async function reportFailure(work) {
22
+ try {
23
+ await work();
24
+ }
25
+ catch (error) {
26
+ console.error(error instanceof Error ? error.message : String(error));
27
+ throw error;
28
+ }
29
+ }
20
30
  let Shoplifter = class Shoplifter {
21
31
  async files() { await previewUpload(); }
22
- async login() { await login(); }
32
+ async login(channelCode) { await reportFailure(() => login(channelCode)); }
23
33
  async logout() { await logout(); }
24
34
  async dependencies(action) { if (action !== 'sync')
25
35
  throw new Error('Choose dependencies sync'); await syncDependencies(); }
@@ -59,7 +69,7 @@ let Shoplifter = class Shoplifter {
59
69
  throw new Error(`Astro exited with status ${code}`);
60
70
  }
61
71
  async publication(action, channel, revision, language) {
62
- await publication(action, channel, revision, language);
72
+ await reportFailure(() => publication(action, channel, revision, language));
63
73
  }
64
74
  };
65
75
  __decorate([
@@ -70,8 +80,9 @@ __decorate([
70
80
  ], Shoplifter.prototype, "files", null);
71
81
  __decorate([
72
82
  command(),
83
+ __param(0, optionalArg('channel-code')),
73
84
  __metadata("design:type", Function),
74
- __metadata("design:paramtypes", []),
85
+ __metadata("design:paramtypes", [String]),
75
86
  __metadata("design:returntype", Promise)
76
87
  ], Shoplifter.prototype, "login", null);
77
88
  __decorate([
@@ -196,7 +207,7 @@ __decorate([
196
207
  ], Shoplifter.prototype, "publication", null);
197
208
  Shoplifter = __decorate([
198
209
  program(),
199
- version('0.1.0'),
210
+ version('0.1.1'),
200
211
  description('Author and publish independent Shoplifter presentations')
201
212
  ], Shoplifter);
202
213
  if (Number(process.versions.node.split('.')[0]) !== 24)
@@ -1,4 +1,4 @@
1
- import { apiOrigin, credentials } from './auth.js';
1
+ import { apiOrigin, credentials, sessionChannel } from './auth.js';
2
2
  import { readProject } from './files.js';
3
3
  import { collectUploadFiles } from './upload-files.js';
4
4
  export async function previewUpload(directory = process.cwd()) {
@@ -11,13 +11,15 @@ export async function publication(action, channel, revision, languageCode) {
11
11
  if (!['upload', 'stage', 'preview', 'preview-link', 'build', 'publish', 'status', 'logs', 'retry', 'retry-deployment', 'activate', 'rollback'].includes(action))
12
12
  throw new Error('Unknown publication operation');
13
13
  const base = apiOrigin(), session = await credentials(false);
14
- if (session && session.channelId !== channel)
15
- throw new Error('CLI session belongs to another channel; run shoplifter login');
14
+ const channelCode = session ? await sessionChannel(session.token) : channel;
15
+ if (session && channel !== channelCode && channel !== session.channelId)
16
+ throw new Error('CLI session belongs to another channel; run shoplifter login with the intended channel code');
16
17
  if (!session && !['localhost', '127.0.0.1', '[::1]'].includes(base.hostname))
17
18
  throw new Error('Run shoplifter login before publishing remotely');
18
19
  if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password)
19
20
  throw new Error('Invalid API origin');
20
- const body = { channelId: channel, revisionId: revision, ...(languageCode ? { languageCode } : {}) };
21
+ const selector = !session && /^\d+$/.test(channel) ? { channelId: channel } : { channelCode };
22
+ const body = { ...selector, revisionId: revision, ...(languageCode ? { languageCode } : {}) };
21
23
  if (action === 'upload') {
22
24
  body.project = await readProject();
23
25
  body.files = (await collectUploadFiles(process.cwd(), body.project)).map(file => ({ path: file.path, bytes: file.bytes.toString('base64') }));
@@ -30,7 +32,7 @@ export async function publication(action, channel, revision, languageCode) {
30
32
  const response = await fetch(new URL(session ? '/shoplifter-cli/operation' : '/shoplifter-development/' + operation, base), {
31
33
  method: 'POST',
32
34
  headers: { 'content-type': 'application/json', ...(session ? { authorization: 'Bearer ' + session.token } : {}) },
33
- body: JSON.stringify(session ? { channelId: channel, languageCode, action: operation, input: body } : body),
35
+ body: JSON.stringify(session ? { channelCode, languageCode, action: operation, input: body } : body),
34
36
  signal: AbortSignal.timeout(120000),
35
37
  });
36
38
  const result = await response.text();
@@ -7,11 +7,11 @@
7
7
  },
8
8
  {
9
9
  "path": "auth.d.ts",
10
- "sha256": "796dd7dad86e0c8865d78e10cdad471948121fa5d838f9f7f88c162d8e3f6734"
10
+ "sha256": "ff98d7cbeca5a9fc2985acf804d18fcfe18e54cdac63a4484367b3e1638d349a"
11
11
  },
12
12
  {
13
13
  "path": "auth.js",
14
- "sha256": "0f91582d678556c8d16f9f6be10993d5a47989914c06358c20ed25c213d90f6a"
14
+ "sha256": "282c5df5cc768f33e2f96052fb21758f74ca3875bc5c9c582c439a490ceed15e"
15
15
  },
16
16
  {
17
17
  "path": "author.d.ts",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  {
37
37
  "path": "index.js",
38
- "sha256": "69c7e98bf5dbcd110c84e975ae90837cd05d8364fbdbdb13bc8ca97f492ee867"
38
+ "sha256": "4b85f13c321650f3a35e9cdee6df2eb8608d815750886b99a5850b224441e3c4"
39
39
  },
40
40
  {
41
41
  "path": "publication.d.ts",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  {
45
45
  "path": "publication.js",
46
- "sha256": "cfba2d5a39d1e1c3f713493f4fd58b886adc26573ee3691a660b23d827ca9d98"
46
+ "sha256": "c9d689a9cc90d61207c4f48c1489fafeda8d1016d7ec0e5a12fb885ddb4abac4"
47
47
  },
48
48
  {
49
49
  "path": "runtime.d.ts",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  {
53
53
  "path": "runtime.js",
54
- "sha256": "134ba8b055ace5ee9ca375915a24848ba1cf677685da051462b6319cc1d637f7"
54
+ "sha256": "f278ffc0d417812adbba6ac0dd742b0c5a3fc91c5089c310c9914dbb8a98f1d2"
55
55
  },
56
56
  {
57
57
  "path": "upload-files.d.ts",
package/dist/runtime.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- import { readFile } from 'node:fs/promises';
3
2
  import { createHash } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
4
  const maximumArchive = 12 * 1024 * 1024;
5
5
  async function bytes(response, maximum) {
6
6
  if (!response.ok || !response.body)
@@ -33,7 +33,10 @@ export async function downloadRuntime(origin) {
33
33
  throw new Error('Invalid platform runtime archive');
34
34
  return runtime;
35
35
  }
36
- const { version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
36
+ // CLI-only patches do not require new platform runtime releases.
37
+ const { shoplifterRuntimeVersion: version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
38
+ if (!/^\d+\.\d+\.\d+$/.test(version ?? ''))
39
+ throw new Error('Invalid platform runtime version');
37
40
  const archive = async (kind) => {
38
41
  const name = '@shoplifter/presentation-' + kind;
39
42
  const metadata = JSON.parse((await bytes(await get(`https://registry.npmjs.org/${encodeURIComponent(name)}/${version}`), 512 * 1024)).toString());
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@shoplifter/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
+ "shoplifterRuntimeVersion": "0.1.0",
4
5
  "private": false,
5
6
  "license": "Apache-2.0",
6
7
  "description": "Author, validate and publish independent Shoplifter Astro presentations",