@quadient/skillshare 1.6.0 → 1.6.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
@@ -276,3 +276,13 @@ This is full-page reload, not React HMR. A CSP that disallows same-origin script
276
276
  or connections must be adjusted for local development or the page reloaded manually.
277
277
  The `/__skillshare_dev/` path is reserved for local development controls. This command
278
278
  does not implement the SkillShare catalog, actual SSO or cloud deployment.
279
+
280
+ ### Worker user directory (1.6.1)
281
+
282
+ Authenticated requests in `pages dev` include `X-Skillshare-Users-Url` and
283
+ `X-Skillshare-Users-Token`. A Worker can fetch the URL with the token in the
284
+ `X-Skillshare-Users-Token` request header to read the four local users. Each entry
285
+ contains `id`, `displayName`, `email`, `isAdmin` and `isOwner`. The hosted backend
286
+ uses the same contract for active SkillShare users. These capabilities stay in
287
+ the Worker and are not injected on `api/bypass-sso/*` requests. See the
288
+ [Worker example](../docs/pages-backends.md#skillshare-user-directory-from-a-worker).
@@ -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.1",
4
4
  "description": "Skillshare CLI for installing and publishing Quadient skills",
5
5
  "type": "module",
6
6
  "bin": {