@3sln/trove 0.0.11 → 0.0.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3sln/trove",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "description": "Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.",
6
6
  "repository": {
@@ -35,6 +35,18 @@
35
35
  // Evaluate URL https://<your drive>/api/access/evaluate
36
36
  // Keys URL https://<your drive>/api/access/keys
37
37
  //
38
+ // 4. AND let Access reach them. If the drive sits behind Access — which is the whole
39
+ // point — then by default these two paths do as well, and Access would have to
40
+ // authenticate to itself to call them. It cannot, so it fails with nothing useful to
41
+ // say. Add a Bypass policy for `/api/access/*`.
42
+ //
43
+ // Bypassing is safe here rather than a concession, and that is by construction: the
44
+ // keys endpoint serves a public key, and the evaluate endpoint authenticates its
45
+ // caller itself by verifying the assertion. Neither ever depended on Access for its
46
+ // own protection. They are also `public: true`, so this drive's OWN identity
47
+ // requirement does not apply to them either — Access holds no Trove session and
48
+ // never will.
49
+ //
38
50
  // WHY IT REFUSES TO RUN UNVERIFIED
39
51
  //
40
52
  // This endpoint answers "does this email have access to this drive". That is a question
@@ -116,6 +128,10 @@ export function externalEvaluation({ privateJwk, team, kid = 'trove-access', jwk
116
128
  method: 'POST',
117
129
  path: '/api/access/evaluate',
118
130
  deps: ['collections'],
131
+ // No Trove identity: the caller is Cloudflare, not a user of this drive, and it
132
+ // authenticates by signing the assertion — which `verifyJwt` below checks. A
133
+ // session requirement here would be asking Access to log in as somebody.
134
+ public: true,
119
135
  async handler(ctx) {
120
136
  const token = await parseAssertion(ctx.req);
121
137
  if (!token) throw TroveError.invalid('No access assertion in the request');
@@ -141,6 +157,7 @@ export function externalEvaluation({ privateJwk, team, kid = 'trove-access', jwk
141
157
  method: 'GET',
142
158
  path: '/api/access/keys',
143
159
  deps: [],
160
+ public: true,
144
161
  // Public by design: it is a public key, and Cloudflare fetches it unauthenticated.
145
162
  handler() {
146
163
  return { keys: [publicJwkOf(privateJwk, { kid })] };
@@ -330,11 +330,18 @@ export async function createServer(config = {}) {
330
330
  // exist. A drive that has not configured one has no `/api/access/*` at all, rather than
331
331
  // endpoints that exist to answer "no" — which is the difference between a feature that is
332
332
  // off and a feature that is broken.
333
+ // Paths a contributed component declared as needing no Trove identity. Exact matches
334
+ // only, and that is deliberate: the check below runs BEFORE routing, so it cannot know
335
+ // which parameterised route would have matched. A component that wants a public route
336
+ // gives it a fixed path.
337
+ const publicPaths = new Set();
338
+
333
339
  const accessPolicy = config.accessEvaluation
334
340
  ? externalEvaluation({ ...config.accessEvaluation, team: config.accessEvaluation.team || config.identity?.access?.team })
335
341
  : null;
336
342
  for (const route of accessPolicy?.routes?.(routeHelpers) || []) {
337
343
  router.add(route.method, route.path, route.deps || [], route.handler);
344
+ if (route.public) publicPaths.add(route.path);
338
345
  }
339
346
 
340
347
  // Said at boot, because that is when someone is looking and can still fix it. The
@@ -409,8 +416,14 @@ export async function createServer(config = {}) {
409
416
  let principal = null;
410
417
  let grant = null;
411
418
  try {
412
- grant = await capabilities.resolve(req);
413
- if (!grant) principal = await identity.authenticate(req);
419
+ // A public route answers to something other than this drive's identity — the
420
+ // external policy endpoints verify a Cloudflare-signed assertion themselves, and
421
+ // the keys endpoint serves a public key. Requiring a session on those is asking
422
+ // the caller to authenticate as a user it is not and does not have.
423
+ if (!publicPaths.has(url.pathname)) {
424
+ grant = await capabilities.resolve(req);
425
+ if (!grant) principal = await identity.authenticate(req);
426
+ }
414
427
  } catch (err) {
415
428
  const e = err instanceof TroveError ? err : TroveError.unauthorized('Authentication failed');
416
429
  return withChallenge(new Response(JSON.stringify(e.toJSON()), { status: e.status, headers: { 'content-type': 'application/json', 'x-content-type-options': 'nosniff' } }), req);