@artenx/dshb 0.1.20 → 0.1.22

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
@@ -12,6 +12,8 @@ Restart the corresponding DSH Web profile after installation.
12
12
 
13
13
  Desktop app: install from **Settings → Plugins → More** in the DSH desktop app; the plugin takes effect on next startup.
14
14
 
15
+ The Authentication settings entry appears in the browser-based Web app. The DSH desktop shell uses the `dsh-app://` protocol and omits this entry; its loopback requests are admitted automatically.
16
+
15
17
  ## Requirements
16
18
 
17
19
  - Node.js 22.13 or newer
package/lib/auth/index.js CHANGED
@@ -44,20 +44,31 @@ function registerFrontendBootRaceWorkaround(ctx) {
44
44
  function apply(ctx) {
45
45
  const store = sharedCredentialStore();
46
46
  const gate = new AuthGate(store);
47
- decorateWebServer(ctx.webServer, store, gate);
47
+ ctx.effect(() => decorateWebServer(ctx.webServer, store, gate));
48
48
  registerFrontendBootRaceWorkaround(ctx);
49
- ctx.webServer.tapIndex(installLoopbackCompat);
49
+ ctx.effect(() => ctx.webServer.tapIndex(installLoopbackCompat));
50
50
  registerAuthRoutes(ctx, store, new LoginRateLimiter());
51
51
  ctx.inject(["connection"], (connCtx) => {
52
- const conn = connCtx.connection;
53
- const auth = conn?.browserAuth;
54
- if (auth && typeof auth.isAuthenticated === "function" && !auth.__dshbNeutralized) {
55
- auth.isAuthenticated = () => true;
56
- auth.__dshbNeutralized = true;
57
- }
58
- if (conn && typeof conn.requestRejection === "function") gate.setAuthRequestTrustCheck((req) => {
59
- const rejection = conn.requestRejection(req);
60
- return rejection === void 0 || rejection === 401;
52
+ connCtx.effect(() => {
53
+ const conn = connCtx.connection;
54
+ const auth = conn?.browserAuth;
55
+ const originalIsAuthenticated = auth?.isAuthenticated;
56
+ const neutralizedIsAuthenticated = () => true;
57
+ let installedNeutralizer = false;
58
+ if (auth && typeof originalIsAuthenticated === "function" && !auth.__dshbNeutralized) {
59
+ auth.isAuthenticated = neutralizedIsAuthenticated;
60
+ auth.__dshbNeutralized = true;
61
+ installedNeutralizer = true;
62
+ }
63
+ if (conn && typeof conn.requestRejection === "function") gate.setAuthRequestTrustCheck((req) => {
64
+ const rejection = conn.requestRejection(req);
65
+ return rejection === void 0 || rejection === 401;
66
+ });
67
+ return () => {
68
+ if (!auth || !installedNeutralizer) return;
69
+ if (auth.isAuthenticated === neutralizedIsAuthenticated && originalIsAuthenticated) auth.isAuthenticated = originalIsAuthenticated;
70
+ delete auth.__dshbNeutralized;
71
+ };
61
72
  });
62
73
  });
63
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { readFileSync, readdirSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { basename, dirname, join } from 'node:path'\nimport type { IncomingMessage } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sharedCredentialStore } from './credentials.js'\nimport { decorateWebServer } from './decorate-webserver.js'\nimport { AuthGate } from './gate.js'\nimport { installLoopbackCompat } from './loopback-compat.js'\nimport { LoginRateLimiter } from './ratelimit.js'\nimport { registerAuthRoutes } from './routes.js'\n\nexport const name = 'dshb-auth'\n\nexport const inject = ['webServer']\n\nfunction registerFrontendBootRaceWorkaround(ctx: Context): void {\n try {\n // Resolve the frontend through the same `dsh-web-app` bundle the webserver\n // loads, so the patched hashed asset is the one the static fallback serves.\n // `process.cwd()` may hold a different installation whose asset filename\n // the server never requests, leaving the served bundle unpatched.\n const resolver = createRequire(import.meta.url)\n let manifestPath: string\n try {\n const webAppManifest = resolver.resolve('@deepseek-ai/dsh-web-app/package.json')\n manifestPath = createRequire(webAppManifest).resolve('@deepseek-ai/dsh-web-frontend/package.json')\n } catch {\n manifestPath = resolver.resolve('@deepseek-ai/dsh-web-frontend/package.json', {\n paths: [process.cwd()],\n })\n }\n const assetDir = join(dirname(manifestPath), 'dist', 'assets')\n const assetName = readdirSync(assetDir).find((name) => /^index-.*\\.js$/.test(name))\n if (!assetName) return\n\n const assetPath = join(assetDir, assetName)\n const source = readFileSync(assetPath, 'utf8')\n // DSH asserts before api-remotes' dynamic remote namespace fibers settle.\n // Delay the assertion so mountApp sees the complete client graph; real\n // activation failures still throw after the bounded wait. The minified\n // identifiers vary between builds, and 0.1.7 moved the assertion into nE.\n const legacyPatch = source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),this\\.assertEntriesActive\\(([A-Za-z_$][\\w$]*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),this.assertEntriesActive($2)',\n )\n const patched = legacyPatch !== source\n ? legacyPatch\n : source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),([A-Za-z_$][\\w$]*)\\(([A-Za-z_$][\\w$]*),([A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),$2($3,$4)',\n )\n if (patched === source) return\n\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'exact',\n path: `/assets/${basename(assetPath)}`,\n handler: (_req, res) => {\n res.writeHead(200, {\n 'cache-control': 'no-store',\n 'content-type': 'application/javascript; charset=utf-8',\n })\n res.end(patched)\n },\n }),\n )\n } catch {\n // Upstream assets are optional from DSHB's perspective. A missing or changed\n // frontend layout simply skips this version-specific workaround.\n }\n}\n\nexport function apply(ctx: Context): void {\n const store = sharedCredentialStore()\n const gate = new AuthGate(store)\n decorateWebServer(ctx.webServer, store, gate)\n registerFrontendBootRaceWorkaround(ctx)\n ctx.webServer.tapIndex(installLoopbackCompat)\n registerAuthRoutes(ctx, store, new LoginRateLimiter())\n\n // DSH 0.1.5 introduced a browser-auth fence on the connection package: the SPA\n // fallback calls connection.authorizeIndex() on \"/\" (mints/checks DSH's own\n // launch-token cookie), and /api routes run requestRejection() which 401s when\n // that DSH cookie is absent. DSHB uses its own session cookie and must stay the\n // sole auth gate (DSH 0.1.1 had no such connection fence, so DSHB worked there).\n // Neutralize the connection's browserAuth.isAuthenticated check so DSH's fence\n // never rejects a DSHB-authenticated (or loopback) request; DSHB's gate still\n // rejects unauthenticated access to gated routes. Property is read at request\n // time, so installing it once after connection is ready suffices.\n ctx.inject(['connection'], (connCtx: Context) => {\n const conn = (connCtx as unknown as {\n connection?: {\n browserAuth?: { isAuthenticated?: (...a: unknown[]) => unknown; __dshbNeutralized?: true }\n requestRejection?: (request: IncomingMessage) => number | undefined\n }\n }).connection\n const auth = conn?.browserAuth\n if (auth && typeof auth.isAuthenticated === 'function' && !auth.__dshbNeutralized) {\n auth.isAuthenticated = () => true\n auth.__dshbNeutralized = true\n }\n if (conn && typeof conn.requestRejection === 'function') {\n gate.setAuthRequestTrustCheck((req) => {\n const rejection = conn.requestRejection!(req)\n // DSH 401 is its browser-cookie gate; DSHB owns that check for these routes.\n return rejection === undefined || rejection === 401\n })\n }\n })\n}\n"],"mappings":";;;;;;;;;;AAYA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,WAAW;AAElC,SAAS,mCAAmC,KAAoB;CAC9D,IAAI;EAKF,MAAM,WAAW,cAAc,YAAY,GAAG;EAC9C,IAAI;EACJ,IAAI;GACF,MAAM,iBAAiB,SAAS,QAAQ,uCAAuC;GAC/E,eAAe,cAAc,cAAc,CAAC,CAAC,QAAQ,4CAA4C;EACnG,QAAQ;GACN,eAAe,SAAS,QAAQ,8CAA8C,EAC5E,OAAO,CAAC,QAAQ,IAAI,CAAC,EACvB,CAAC;EACH;EACA,MAAM,WAAW,KAAK,QAAQ,YAAY,GAAG,QAAQ,QAAQ;EAC7D,MAAM,YAAY,YAAY,QAAQ,CAAC,CAAC,MAAM,SAAS,iBAAiB,KAAK,IAAI,CAAC;EAClF,IAAI,CAAC,WAAW;EAEhB,MAAM,YAAY,KAAK,UAAU,SAAS;EAC1C,MAAM,SAAS,aAAa,WAAW,MAAM;EAK7C,MAAM,cAAc,OAAO,QACzB,uFACA,wFACF;EACA,MAAM,UAAU,gBAAgB,SAC5B,cACA,OAAO,QACL,0HACA,qEACF;EACJ,IAAI,YAAY,QAAQ;EAExB,IAAI,aACF,IAAI,UAAU,SAAS;GACrB,MAAM;GACN,MAAM,WAAW,SAAS,SAAS;GACnC,UAAU,MAAM,QAAQ;IACtB,IAAI,UAAU,KAAK;KACjB,iBAAiB;KACjB,gBAAgB;IAClB,CAAC;IACD,IAAI,IAAI,OAAO;GACjB;EACF,CAAC,CACH;CACF,QAAQ,CAGR;AACF;AAEA,SAAgB,MAAM,KAAoB;CACxC,MAAM,QAAQ,sBAAsB;CACpC,MAAM,OAAO,IAAI,SAAS,KAAK;CAC/B,kBAAkB,IAAI,WAAW,OAAO,IAAI;CAC5C,mCAAmC,GAAG;CACtC,IAAI,UAAU,SAAS,qBAAqB;CAC5C,mBAAmB,KAAK,OAAO,IAAI,iBAAiB,CAAC;CAWrD,IAAI,OAAO,CAAC,YAAY,IAAI,YAAqB;EAC/C,MAAM,OAAQ,QAKX;EACH,MAAM,OAAO,MAAM;EACnB,IAAI,QAAQ,OAAO,KAAK,oBAAoB,cAAc,CAAC,KAAK,mBAAmB;GACjF,KAAK,wBAAwB;GAC7B,KAAK,oBAAoB;EAC3B;EACA,IAAI,QAAQ,OAAO,KAAK,qBAAqB,YAC3C,KAAK,0BAA0B,QAAQ;GACrC,MAAM,YAAY,KAAK,iBAAkB,GAAG;GAE5C,OAAO,cAAc,KAAA,KAAa,cAAc;EAClD,CAAC;CAEL,CAAC;AACH"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { readFileSync, readdirSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { basename, dirname, join } from 'node:path'\nimport type { IncomingMessage } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sharedCredentialStore } from './credentials.js'\nimport { decorateWebServer } from './decorate-webserver.js'\nimport { AuthGate } from './gate.js'\nimport { installLoopbackCompat } from './loopback-compat.js'\nimport { LoginRateLimiter } from './ratelimit.js'\nimport { registerAuthRoutes } from './routes.js'\n\nexport const name = 'dshb-auth'\n\nexport const inject = ['webServer']\n\nfunction registerFrontendBootRaceWorkaround(ctx: Context): void {\n try {\n // Resolve the frontend through the same `dsh-web-app` bundle the webserver\n // loads, so the patched hashed asset is the one the static fallback serves.\n // `process.cwd()` may hold a different installation whose asset filename\n // the server never requests, leaving the served bundle unpatched.\n const resolver = createRequire(import.meta.url)\n let manifestPath: string\n try {\n const webAppManifest = resolver.resolve('@deepseek-ai/dsh-web-app/package.json')\n manifestPath = createRequire(webAppManifest).resolve('@deepseek-ai/dsh-web-frontend/package.json')\n } catch {\n manifestPath = resolver.resolve('@deepseek-ai/dsh-web-frontend/package.json', {\n paths: [process.cwd()],\n })\n }\n const assetDir = join(dirname(manifestPath), 'dist', 'assets')\n const assetName = readdirSync(assetDir).find((name) => /^index-.*\\.js$/.test(name))\n if (!assetName) return\n\n const assetPath = join(assetDir, assetName)\n const source = readFileSync(assetPath, 'utf8')\n // DSH asserts before api-remotes' dynamic remote namespace fibers settle.\n // Delay the assertion so mountApp sees the complete client graph; real\n // activation failures still throw after the bounded wait. The minified\n // identifiers vary between builds, and 0.1.7 moved the assertion into nE.\n const legacyPatch = source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),this\\.assertEntriesActive\\(([A-Za-z_$][\\w$]*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),this.assertEntriesActive($2)',\n )\n const patched = legacyPatch !== source\n ? legacyPatch\n : source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),([A-Za-z_$][\\w$]*)\\(([A-Za-z_$][\\w$]*),([A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),$2($3,$4)',\n )\n if (patched === source) return\n\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'exact',\n path: `/assets/${basename(assetPath)}`,\n handler: (_req, res) => {\n res.writeHead(200, {\n 'cache-control': 'no-store',\n 'content-type': 'application/javascript; charset=utf-8',\n })\n res.end(patched)\n },\n }),\n )\n } catch {\n // Upstream assets are optional from DSHB's perspective. A missing or changed\n // frontend layout simply skips this version-specific workaround.\n }\n}\n\nexport function apply(ctx: Context): void {\n const store = sharedCredentialStore()\n const gate = new AuthGate(store)\n ctx.effect(() => decorateWebServer(ctx.webServer, store, gate))\n registerFrontendBootRaceWorkaround(ctx)\n ctx.effect(() => ctx.webServer.tapIndex(installLoopbackCompat))\n registerAuthRoutes(ctx, store, new LoginRateLimiter())\n\n // DSH 0.1.5 introduced a browser-auth fence on the connection package: the SPA\n // fallback calls connection.authorizeIndex() on \"/\" (mints/checks DSH's own\n // launch-token cookie), and /api routes run requestRejection() which 401s when\n // that DSH cookie is absent. DSHB uses its own session cookie and must stay the\n // sole auth gate (DSH 0.1.1 had no such connection fence, so DSHB worked there).\n // Neutralize the connection's browserAuth.isAuthenticated check so DSH's fence\n // never rejects a DSHB-authenticated (or loopback) request; DSHB's gate still\n // rejects unauthenticated access to gated routes. Property is read at request\n // time, so installing it once after connection is ready suffices.\n ctx.inject(['connection'], (connCtx: Context) => {\n connCtx.effect(() => {\n const conn = (connCtx as unknown as {\n connection?: {\n browserAuth?: { isAuthenticated?: (...a: unknown[]) => unknown; __dshbNeutralized?: true }\n requestRejection?: (request: IncomingMessage) => number | undefined\n }\n }).connection\n const auth = conn?.browserAuth\n const originalIsAuthenticated = auth?.isAuthenticated\n const neutralizedIsAuthenticated = () => true\n let installedNeutralizer = false\n if (auth && typeof originalIsAuthenticated === 'function' && !auth.__dshbNeutralized) {\n auth.isAuthenticated = neutralizedIsAuthenticated\n auth.__dshbNeutralized = true\n installedNeutralizer = true\n }\n if (conn && typeof conn.requestRejection === 'function') {\n gate.setAuthRequestTrustCheck((req) => {\n const rejection = conn.requestRejection!(req)\n // DSH 401 is its browser-cookie gate; DSHB owns that check for these routes.\n return rejection === undefined || rejection === 401\n })\n }\n return () => {\n if (!auth || !installedNeutralizer) return\n if (auth.isAuthenticated === neutralizedIsAuthenticated && originalIsAuthenticated) {\n auth.isAuthenticated = originalIsAuthenticated\n }\n delete auth.__dshbNeutralized\n }\n })\n })\n}\n"],"mappings":";;;;;;;;;;AAYA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,WAAW;AAElC,SAAS,mCAAmC,KAAoB;CAC9D,IAAI;EAKF,MAAM,WAAW,cAAc,YAAY,GAAG;EAC9C,IAAI;EACJ,IAAI;GACF,MAAM,iBAAiB,SAAS,QAAQ,uCAAuC;GAC/E,eAAe,cAAc,cAAc,CAAC,CAAC,QAAQ,4CAA4C;EACnG,QAAQ;GACN,eAAe,SAAS,QAAQ,8CAA8C,EAC5E,OAAO,CAAC,QAAQ,IAAI,CAAC,EACvB,CAAC;EACH;EACA,MAAM,WAAW,KAAK,QAAQ,YAAY,GAAG,QAAQ,QAAQ;EAC7D,MAAM,YAAY,YAAY,QAAQ,CAAC,CAAC,MAAM,SAAS,iBAAiB,KAAK,IAAI,CAAC;EAClF,IAAI,CAAC,WAAW;EAEhB,MAAM,YAAY,KAAK,UAAU,SAAS;EAC1C,MAAM,SAAS,aAAa,WAAW,MAAM;EAK7C,MAAM,cAAc,OAAO,QACzB,uFACA,wFACF;EACA,MAAM,UAAU,gBAAgB,SAC5B,cACA,OAAO,QACL,0HACA,qEACF;EACJ,IAAI,YAAY,QAAQ;EAExB,IAAI,aACF,IAAI,UAAU,SAAS;GACrB,MAAM;GACN,MAAM,WAAW,SAAS,SAAS;GACnC,UAAU,MAAM,QAAQ;IACtB,IAAI,UAAU,KAAK;KACjB,iBAAiB;KACjB,gBAAgB;IAClB,CAAC;IACD,IAAI,IAAI,OAAO;GACjB;EACF,CAAC,CACH;CACF,QAAQ,CAGR;AACF;AAEA,SAAgB,MAAM,KAAoB;CACxC,MAAM,QAAQ,sBAAsB;CACpC,MAAM,OAAO,IAAI,SAAS,KAAK;CAC/B,IAAI,aAAa,kBAAkB,IAAI,WAAW,OAAO,IAAI,CAAC;CAC9D,mCAAmC,GAAG;CACtC,IAAI,aAAa,IAAI,UAAU,SAAS,qBAAqB,CAAC;CAC9D,mBAAmB,KAAK,OAAO,IAAI,iBAAiB,CAAC;CAWrD,IAAI,OAAO,CAAC,YAAY,IAAI,YAAqB;EAC/C,QAAQ,aAAa;GACnB,MAAM,OAAQ,QAKX;GACH,MAAM,OAAO,MAAM;GACnB,MAAM,0BAA0B,MAAM;GACtC,MAAM,mCAAmC;GACzC,IAAI,uBAAuB;GAC3B,IAAI,QAAQ,OAAO,4BAA4B,cAAc,CAAC,KAAK,mBAAmB;IACpF,KAAK,kBAAkB;IACvB,KAAK,oBAAoB;IACzB,uBAAuB;GACzB;GACA,IAAI,QAAQ,OAAO,KAAK,qBAAqB,YAC3C,KAAK,0BAA0B,QAAQ;IACrC,MAAM,YAAY,KAAK,iBAAkB,GAAG;IAE5C,OAAO,cAAc,KAAA,KAAa,cAAc;GAClD,CAAC;GAEH,aAAa;IACX,IAAI,CAAC,QAAQ,CAAC,sBAAsB;IACpC,IAAI,KAAK,oBAAoB,8BAA8B,yBACzD,KAAK,kBAAkB;IAEzB,OAAO,KAAK;GACd;EACF,CAAC;CACH,CAAC;AACH"}
package/lib/client.js CHANGED
@@ -444,6 +444,7 @@ window.__ModuleLoader__.load({
444
444
  configurable: true,
445
445
  get: () => true
446
446
  });
447
+ if (typeof location !== "undefined" && ["dsh-app:", "file:"].includes(location.protocol)) return;
447
448
  ctx.plugin({
448
449
  inject: ["slots"],
449
450
  apply: (sub) => {