sh3-server 0.10.2 → 0.10.5

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/app/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>SH3</title>
7
- <script type="module" crossorigin src="/assets/index-Boy8llCh.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-TUefqqjg.css">
7
+ <script type="module" crossorigin src="/assets/index-C0N0oGGx.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BOrfojaa.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
@@ -67,5 +67,6 @@ export declare class TenantDocStore {
67
67
  applyFromPeer(tenant: string, input: ApplyFromPeerInput): Promise<ApplyResult>;
68
68
  listConflicts(tenant: string): Promise<ConflictRef[]>;
69
69
  readConflict(tenant: string, shardId: string, path: string): Promise<ConflictFile | null>;
70
+ readBranchContent(tenant: string, shardId: string, path: string, origin: string): Promise<string | null>;
70
71
  resolveConflict(tenant: string, shardId: string, path: string, choice: 'local' | string | Buffer): Promise<void>;
71
72
  }
@@ -306,6 +306,21 @@ export class TenantDocStore {
306
306
  async readConflict(tenant, shardId, path) {
307
307
  return this.#conflicts.read(tenant, shardId, path);
308
308
  }
309
+ async readBranchContent(tenant, shardId, path, origin) {
310
+ const cf = await this.#conflicts.read(tenant, shardId, path);
311
+ if (!cf)
312
+ return null;
313
+ const branch = cf.branches.find((b) => b.origin === origin);
314
+ if (!branch)
315
+ return null;
316
+ const content = branch.content;
317
+ if (typeof content === 'string')
318
+ return content;
319
+ if (content && typeof content.toString === 'function') {
320
+ return content.toString('utf-8');
321
+ }
322
+ return null;
323
+ }
309
324
  async resolveConflict(tenant, shardId, path, choice) {
310
325
  const cf = await this.#conflicts.read(tenant, shardId, path);
311
326
  if (!cf)
@@ -44,6 +44,27 @@ export function createDocsRouter(store, settings) {
44
44
  return c.notFound();
45
45
  return c.json(await store.list(tenant, shard));
46
46
  });
47
+ // Branch content read — conflict-branch by origin. Registered before the
48
+ // generic read handler so the `/branch` suffix isn't swallowed as a path.
49
+ router.get('/:tenant/:shard/*', async (c, next) => {
50
+ const { tenant, shard } = c.req.param();
51
+ const prefix = `/api/docs/${tenant}/${shard}/`;
52
+ const rawPath = c.req.path.replace(prefix, '');
53
+ if (!rawPath.endsWith('/branch'))
54
+ return next();
55
+ if (isReservedShardId(shard))
56
+ return c.notFound();
57
+ const filePath = rawPath.replace(/\/branch$/, '');
58
+ if (!filePath)
59
+ return c.json({ error: 'Missing file path' }, 400);
60
+ const origin = c.req.query('origin');
61
+ if (!origin)
62
+ return c.json({ error: 'Missing origin query param' }, 400);
63
+ const content = await store.readBranchContent(tenant, shard, filePath, origin);
64
+ if (content === null)
65
+ return c.notFound();
66
+ return c.text(content);
67
+ });
47
68
  // Read
48
69
  router.get('/:tenant/:shard/*', async (c) => {
49
70
  const { tenant, shard } = c.req.param();
@@ -72,6 +72,24 @@ function makeTenantDocumentAPI(store, tenant, callingShardId, permissions) {
72
72
  resolveConflict: (shardId, path, choice) => store.resolveConflict(tenant, shardId, path, choice),
73
73
  };
74
74
  }
75
+ /**
76
+ * Bootstrap middleware installed on every shard sub-app before its routes
77
+ * are wired. Copies `session` / `caller` from `c.env` (forwarded by the
78
+ * outer handler) into the sub-app's context variables, so scope guards
79
+ * reading `c.get('caller')` see the same identity the outer cascade
80
+ * resolved. Without this, the sub-app's fresh context is empty and
81
+ * `adminOnly` / `scopeRequired` / `tenantRequired` fail regardless of role.
82
+ */
83
+ function contextBootstrap() {
84
+ return async (c, next) => {
85
+ const env = c.env;
86
+ if (env?.session !== undefined)
87
+ c.set('session', env.session);
88
+ if (env?.caller !== undefined)
89
+ c.set('caller', env.caller);
90
+ await next();
91
+ };
92
+ }
75
93
  /**
76
94
  * Dynamic shard route manager. Holds a Map of shard Hono sub-apps
77
95
  * and delegates requests from a single wildcard route.
@@ -90,6 +108,7 @@ export class ShardRouter {
90
108
  throw new Error(`${shardId}/server.js invalid export shape — expected { id, routes }`);
91
109
  }
92
110
  const router = new Hono();
111
+ router.use('*', contextBootstrap());
93
112
  await shard.routes(router, this.#buildContext(shard.id, ctx));
94
113
  mkdirSync(join(ctx.pkgDir, 'data'), { recursive: true });
95
114
  this.shards.set(shardId, { app: router, shard });
@@ -105,6 +124,7 @@ export class ShardRouter {
105
124
  throw new Error(`${shardId} static mount — expected { id, routes }, got ${typeof mod}`);
106
125
  }
107
126
  const router = new Hono();
127
+ router.use('*', contextBootstrap());
108
128
  await mod.routes(router, this.#buildContext(mod.id, ctx));
109
129
  mkdirSync(join(ctx.pkgDir, 'data'), { recursive: true });
110
130
  this.shards.set(shardId, { app: router, shard: mod });
@@ -193,8 +213,15 @@ export class ShardRouter {
193
213
  // @ts-expect-error duplex needed for streaming bodies
194
214
  duplex: 'half',
195
215
  });
196
- // Forward session from upstream sessionAuth so shard middleware can see it
197
- const env = { ...c.env, session: c.get('session') ?? null };
216
+ // Forward session and caller from the outer sessionAuth + resolveCaller
217
+ // cascade. `contextBootstrap` inside the sub-app copies these into
218
+ // `c.var` so scope guards (adminOnly / scopeRequired / tenantRequired)
219
+ // resolve against the same identity as routes mounted on the main tree.
220
+ const env = {
221
+ ...c.env,
222
+ session: c.get('session') ?? null,
223
+ caller: c.get('caller') ?? null,
224
+ };
198
225
  return await entry.app.fetch(strippedRequest, env);
199
226
  }
200
227
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-server",
3
- "version": "0.10.2",
3
+ "version": "0.10.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sh3-server": "dist/cli.js"