@quadient/skillshare 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,9 @@ bun run index.ts install --skill-version-id 42 --target ./installed
21
21
 
22
22
  ## Behavior
23
23
 
24
+ - Starting with 1.6.2, `login`, `git configure` and API commands without a stored session default to `https://skillshare.quadientcloudpreprod.eu`. `login local` and `git configure local` still select `http://localhost:5050`.
25
+ - Existing sessions keep their saved server. After preprod cutover, run `skillshare login` again to replace a UAT session with a preprod session; stored tokens are not reused across hosts.
26
+ - Uploaded/installed records retain their registry URL. Legacy records without a URL still belong to UAT and are not silently reassigned to preprod.
24
27
  - Upload and publish commands show progress while scanning and reading local files, then keep a timed upload status visible until the registry call finishes.
25
28
  - `update` reads the centralized install registry and refreshes recorded installs to the latest published registry versions.
26
29
  - `update` can be narrowed to one skill or one target directory, and `--dry-run` prints planned changes without downloading or overwriting files.
@@ -36,7 +39,7 @@ flowchart TD
36
39
  B -- Yes --> D{Which command?}
37
40
 
38
41
  D -->|login local| E[login LOCAL_URL]
39
- D -->|login| F[login PROD_URL]
42
+ D -->|login| F[login DEFAULT_URL]
40
43
  D -->|logout| G[logoutSession]
41
44
  D -->|whoami list show upload install update| H[createClient using active baseUrl]
42
45
 
@@ -148,7 +151,7 @@ export SKILLSHARE_TOKEN="<personal-access-token>"
148
151
  npx @quadient/skillshare whoami
149
152
  ```
150
153
 
151
- Register the production or local SkillShare host as a generic OAuth provider in Git Credential
154
+ Register the default preprod or local SkillShare host as a generic OAuth provider in Git Credential
152
155
  Manager 2.0.931 or newer:
153
156
 
154
157
  ```bash
@@ -179,7 +182,7 @@ npx @quadient/skillshare pages show release-notes
179
182
  ```
180
183
 
181
184
  The page is then available at
182
- `https://skillshare.inspireclouduat.net/p/release-notes/` after SSO sign-in.
185
+ `https://skillshare.quadientcloudpreprod.eu/p/release-notes/` after SSO sign-in.
183
186
 
184
187
  ## Package Distribution
185
188
 
@@ -276,3 +279,13 @@ This is full-page reload, not React HMR. A CSP that disallows same-origin script
276
279
  or connections must be adjusted for local development or the page reloaded manually.
277
280
  The `/__skillshare_dev/` path is reserved for local development controls. This command
278
281
  does not implement the SkillShare catalog, actual SSO or cloud deployment.
282
+
283
+ ### Worker user directory (1.6.1)
284
+
285
+ Authenticated requests in `pages dev` include `X-Skillshare-Users-Url` and
286
+ `X-Skillshare-Users-Token`. A Worker can fetch the URL with the token in the
287
+ `X-Skillshare-Users-Token` request header to read the four local users. Each entry
288
+ contains `id`, `displayName`, `email`, `isAdmin` and `isOwner`. The hosted backend
289
+ uses the same contract for active SkillShare users. These capabilities stay in
290
+ the Worker and are not injected on `api/bypass-sso/*` requests. See the
291
+ [Worker example](../docs/pages-backends.md#skillshare-user-directory-from-a-worker).
package/dist/index.js CHANGED
@@ -8,7 +8,9 @@ import { mkdir, open, readFile, readdir, rm, stat, writeFile } from 'node:fs/pro
8
8
  import { basename, dirname, join, relative, resolve } from 'node:path';
9
9
  import { homedir } from 'node:os';
10
10
  import { cancel, confirm, intro, isCancel, multiselect, outro, progress, select, spinner, text } from '@clack/prompts';
11
- const PROD_URL = 'https://skillshare.inspireclouduat.net';
11
+ const DEFAULT_URL = 'https://skillshare.quadientcloudpreprod.eu';
12
+ // Records without a registry URL were created against UAT before the preprod migration.
13
+ const LEGACY_UAT_URL = 'https://skillshare.inspireclouduat.net';
12
14
  const LOCAL_URL = 'http://localhost:5050';
13
15
  const APP_DIR = getAppDir();
14
16
  const SESSION_PATH = join(APP_DIR, 'session.json');
@@ -91,6 +93,9 @@ Commands:
91
93
 
92
94
  Options:
93
95
  --token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
96
+
97
+ Default server: ${DEFAULT_URL}
98
+ A stored session keeps its server; run "skillshare login" to switch to the default.
94
99
  `;
95
100
  async function main() {
96
101
  const args = process.argv.slice(2);
@@ -110,7 +115,7 @@ async function main() {
110
115
  }
111
116
  switch (command) {
112
117
  case 'login': {
113
- await login(options.positionals[0] === 'local' ? LOCAL_URL : PROD_URL);
118
+ await login(options.positionals[0] === 'local' ? LOCAL_URL : DEFAULT_URL);
114
119
  return;
115
120
  }
116
121
  case 'logout': {
@@ -119,7 +124,7 @@ async function main() {
119
124
  }
120
125
  case 'git': {
121
126
  const action = options.positionals[0];
122
- const baseUrl = options.positionals[1] === 'local' ? LOCAL_URL : PROD_URL;
127
+ const baseUrl = options.positionals[1] === 'local' ? LOCAL_URL : DEFAULT_URL;
123
128
  if (action === 'configure') {
124
129
  await configureGitCredentialManager(baseUrl);
125
130
  return;
@@ -1455,7 +1460,7 @@ async function resolveActiveBaseUrl() {
1455
1460
  if (session?.baseUrl) {
1456
1461
  return normalizeBaseUrl(session.baseUrl);
1457
1462
  }
1458
- return PROD_URL;
1463
+ return DEFAULT_URL;
1459
1464
  }
1460
1465
  async function createClient(baseUrl, explicitToken) {
1461
1466
  const resolvedBaseUrl = normalizeBaseUrl(baseUrl);
@@ -1823,7 +1828,7 @@ async function readUploadedSkillRecords() {
1823
1828
  const normalizedSkillId = Number.isInteger(skillId) && skillId > 0 ? skillId : undefined;
1824
1829
  return [{
1825
1830
  directory: typeof entry.directory === 'string' ? resolve(entry.directory) : '',
1826
- baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : PROD_URL,
1831
+ baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : LEGACY_UAT_URL,
1827
1832
  updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
1828
1833
  skillId: normalizedSkillId,
1829
1834
  skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
@@ -1844,7 +1849,7 @@ async function readInstalledSkillRecords() {
1844
1849
  }
1845
1850
  return [{
1846
1851
  targetRoot: typeof entry.targetRoot === 'string' ? resolve(entry.targetRoot) : '',
1847
- baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : PROD_URL,
1852
+ baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : LEGACY_UAT_URL,
1848
1853
  updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
1849
1854
  skillId,
1850
1855
  skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
@@ -1,4 +1,5 @@
1
1
  import { createServer, request as httpRequest, } from 'node:http';
2
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
2
3
  const sessionCookie = 'skillshare.dev-user';
3
4
  const internal = '/__skillshare_dev';
4
5
  const hostCookie = (name) => name.startsWith('skillshare.auth') ||
@@ -25,7 +26,7 @@ function cleanHeaders(headers) {
25
26
  delete result[name];
26
27
  return result;
27
28
  }
28
- export function requestHeaders(request, user, baseUrl, slug) {
29
+ export function requestHeaders(request, user, baseUrl, slug, usersToken) {
29
30
  const headers = cleanHeaders(request.headers);
30
31
  for (const name of Object.keys(headers))
31
32
  if (name.startsWith('x-skillshare-') ||
@@ -37,6 +38,10 @@ export function requestHeaders(request, user, baseUrl, slug) {
37
38
  headers['accept-encoding'] = 'identity';
38
39
  if (user) {
39
40
  delete headers.authorization;
41
+ if (usersToken) {
42
+ headers['x-skillshare-users-url'] = new URL(`/api/pages/${encodeURIComponent(slug)}/users-directory`, baseUrl).href;
43
+ headers['x-skillshare-users-token'] = usersToken;
44
+ }
40
45
  headers['x-skillshare-user'] = Buffer.from(JSON.stringify(user)).toString('base64');
41
46
  const cookies = (request.headers.cookie ?? '')
42
47
  .split(';')
@@ -74,6 +79,7 @@ export function responseCookies(cookies, prefix) {
74
79
  }
75
80
  export function createDevProxy(options) {
76
81
  const prefix = `/p/${options.slug}`;
82
+ const usersToken = randomBytes(32).toString('hex');
77
83
  const streams = new Set();
78
84
  const upgrades = new Set();
79
85
  const userFor = (request) => {
@@ -98,6 +104,22 @@ export function createDevProxy(options) {
98
104
  return;
99
105
  }
100
106
  const url = new URL(request.url, `http://${request.headers.host}`);
107
+ if (url.pathname === `/api/pages/${encodeURIComponent(options.slug)}/users-directory`) {
108
+ response.setHeader('Cache-Control', 'no-store');
109
+ const token = request.headers['x-skillshare-users-token'];
110
+ if (typeof token !== 'string' || !/^[a-f0-9]{64}$/.test(token) ||
111
+ !timingSafeEqual(Buffer.from(token), Buffer.from(usersToken))) {
112
+ response.writeHead(401).end();
113
+ return;
114
+ }
115
+ if (request.method !== 'GET') {
116
+ response.writeHead(405).end();
117
+ return;
118
+ }
119
+ response.writeHead(200, { 'Content-Type': 'application/json' });
120
+ response.end(JSON.stringify(options.users.map(({ id, displayName, email, isAdmin, isOwner }) => ({ id, displayName, email, isAdmin, isOwner }))));
121
+ return;
122
+ }
101
123
  if (url.pathname === '/' || url.pathname === prefix) {
102
124
  response.writeHead(307, { Location: prefix + '/' + url.search }).end();
103
125
  return;
@@ -188,7 +210,7 @@ label.append(select); bar.append(label); document.body.append(bar);
188
210
  const user = bypass ? undefined : userFor(request);
189
211
  const upstream = httpRequest(new URL(path + url.search, target), {
190
212
  method: request.method,
191
- headers: requestHeaders(request, user, url.origin + prefix + '/', options.slug),
213
+ headers: requestHeaders(request, user, url.origin + prefix + '/', options.slug, usersToken),
192
214
  }, (incoming) => {
193
215
  const headers = cleanHeaders(incoming.headers);
194
216
  headers['cache-control'] = 'private, no-cache';
@@ -251,7 +273,7 @@ label.append(select); bar.append(label); document.body.append(bar);
251
273
  return;
252
274
  }
253
275
  const bypass = path === '/api/bypass-sso' || path.startsWith('/api/bypass-sso/');
254
- const headers = requestHeaders(request, bypass ? undefined : userFor(request), url.origin + prefix + '/', options.slug);
276
+ const headers = requestHeaders(request, bypass ? undefined : userFor(request), url.origin + prefix + '/', options.slug, usersToken);
255
277
  headers.connection = 'Upgrade';
256
278
  headers.upgrade = request.headers.upgrade;
257
279
  const upstream = httpRequest(new URL(path + url.search, target), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quadient/skillshare",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Skillshare CLI for installing and publishing Quadient skills",
5
5
  "type": "module",
6
6
  "bin": {