@intx/hub-api 0.1.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 +29 -0
- package/package.json +28 -0
- package/src/app.test.ts +225 -0
- package/src/app.ts +382 -0
- package/src/auth.ts +21 -0
- package/src/context.ts +38 -0
- package/src/format.ts +9 -0
- package/src/git-http/advertise-refs.test.ts +459 -0
- package/src/git-http/advertise-refs.ts +226 -0
- package/src/git-http/pkt-line.test.ts +220 -0
- package/src/git-http/pkt-line.ts +235 -0
- package/src/git-http/receive-pack.test.ts +397 -0
- package/src/git-http/receive-pack.ts +261 -0
- package/src/git-http/side-band-64k.test.ts +181 -0
- package/src/git-http/side-band-64k.ts +134 -0
- package/src/git-http/upload-pack.test.ts +545 -0
- package/src/git-http/upload-pack.ts +396 -0
- package/src/index.ts +23 -0
- package/src/middleware/git-token-auth.test.ts +587 -0
- package/src/middleware/git-token-auth.ts +315 -0
- package/src/middleware/grant.ts +106 -0
- package/src/middleware/session.ts +13 -0
- package/src/middleware/tenant.test.ts +192 -0
- package/src/middleware/tenant.ts +101 -0
- package/src/openapi.ts +66 -0
- package/src/pagination.ts +117 -0
- package/src/routes/agent-data.ts +179 -0
- package/src/routes/agent-state-git.ts +562 -0
- package/src/routes/agents.test.ts +337 -0
- package/src/routes/agents.ts +704 -0
- package/src/routes/approvals.ts +130 -0
- package/src/routes/assets.test.ts +567 -0
- package/src/routes/assets.ts +592 -0
- package/src/routes/credentials.ts +435 -0
- package/src/routes/git-tokens.test.ts +709 -0
- package/src/routes/git-tokens.ts +771 -0
- package/src/routes/grants.ts +509 -0
- package/src/routes/instances.test.ts +1103 -0
- package/src/routes/instances.ts +1797 -0
- package/src/routes/me.ts +405 -0
- package/src/routes/oauth-clients.ts +349 -0
- package/src/routes/observability.ts +146 -0
- package/src/routes/offerings.ts +382 -0
- package/src/routes/principals.ts +515 -0
- package/src/routes/providers.ts +351 -0
- package/src/routes/roles.ts +452 -0
- package/src/routes/sidecars.ts +221 -0
- package/src/routes/tenant-federation.ts +225 -0
- package/src/routes/tenants.ts +369 -0
- package/src/routes/wallets.ts +370 -0
- package/src/session.ts +44 -0
- package/src/timeline-reconstruction.test.ts +786 -0
- package/src/timeline-reconstruction.ts +383 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart-HTTP `git-upload-pack` request handler. Parses the
|
|
3
|
+
* `want`/`have`/`done` pkt-line body, builds an `includeSha`
|
|
4
|
+
* predicate from the principal's `tokenClaims.refPattern` (a SHA is
|
|
5
|
+
* included only if reachable from at least one matching ref), and
|
|
6
|
+
* streams the negotiated packfile back wrapped in side-band-64k
|
|
7
|
+
* channel-1 frames.
|
|
8
|
+
*
|
|
9
|
+
* Denial during negotiation is reported as a pkt-line `ERR <msg>\n`
|
|
10
|
+
* frame, never as a non-200 HTTP status: stock git surfaces the ERR
|
|
11
|
+
* payload as `remote: <msg>; fatal: protocol error`, which is the
|
|
12
|
+
* protocol-correct shape for refPattern denial and unknown-ref
|
|
13
|
+
* rejections. (Receive-pack uses `ng` during report-status instead;
|
|
14
|
+
* that vocabulary is not used here.)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import git from "isomorphic-git";
|
|
19
|
+
import type { RepoId } from "@intx/types/sidecar";
|
|
20
|
+
import { glob } from "@intx/hub-common";
|
|
21
|
+
import {
|
|
22
|
+
createNegotiatedPack,
|
|
23
|
+
collectReachableObjects,
|
|
24
|
+
} from "@intx/storage-isogit";
|
|
25
|
+
import { readPktLine, writePktLine, writeFlush, writeErr } from "./pkt-line";
|
|
26
|
+
import { chunkPackToSideBand } from "./side-band-64k";
|
|
27
|
+
import type { RefEntry } from "./advertise-refs";
|
|
28
|
+
|
|
29
|
+
export const UPLOAD_PACK_RESULT_CONTENT_TYPE =
|
|
30
|
+
"application/x-git-upload-pack-result";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Principal contract consumed by the upload-pack handler. Narrowed to
|
|
34
|
+
* the single field the handler reads (`tokenClaims.refPattern`) so any
|
|
35
|
+
* principal carrying that claim satisfies the type without coupling to
|
|
36
|
+
* the full user-principal shape.
|
|
37
|
+
*/
|
|
38
|
+
export type UploadPackPrincipal = {
|
|
39
|
+
readonly kind: string;
|
|
40
|
+
readonly tokenClaims: {
|
|
41
|
+
readonly refPattern: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Repository-store capability the upload-pack handler depends on. The
|
|
47
|
+
* substrate or a repo-direct adapter implements this; the handler does
|
|
48
|
+
* not care which. `listRefs` provides the ref names + tip SHAs the
|
|
49
|
+
* refPattern is matched against; `getRepoDir` returns the on-disk path
|
|
50
|
+
* passed to `createNegotiatedPack`.
|
|
51
|
+
*/
|
|
52
|
+
export interface UploadPackRepoStore {
|
|
53
|
+
listRefs(principal: UploadPackPrincipal, repoId: RepoId): Promise<RefEntry[]>;
|
|
54
|
+
getRepoDir(principal: UploadPackPrincipal, repoId: RepoId): Promise<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ParsedRequest = {
|
|
58
|
+
wants: string[];
|
|
59
|
+
haves: string[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
async function parseUploadRequest(body: Uint8Array): Promise<ParsedRequest> {
|
|
63
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
64
|
+
start(controller) {
|
|
65
|
+
if (body.length > 0) controller.enqueue(body);
|
|
66
|
+
controller.close();
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const reader = stream.getReader();
|
|
70
|
+
const wants: string[] = [];
|
|
71
|
+
const haves: string[] = [];
|
|
72
|
+
const dec = new TextDecoder();
|
|
73
|
+
let seenFlush = false;
|
|
74
|
+
for (;;) {
|
|
75
|
+
const frame = await readPktLine(reader);
|
|
76
|
+
if (frame === null) break;
|
|
77
|
+
if (frame.kind === "flush") {
|
|
78
|
+
seenFlush = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (frame.kind === "delim") continue;
|
|
82
|
+
const line = dec.decode(frame.payload).replace(/\n$/, "");
|
|
83
|
+
if (!seenFlush) {
|
|
84
|
+
if (line.startsWith("want ")) {
|
|
85
|
+
const rest = line.slice("want ".length);
|
|
86
|
+
const sha = rest.split(" ", 1)[0];
|
|
87
|
+
if (sha === undefined || sha.length === 0) {
|
|
88
|
+
throw new Error(`upload-pack: malformed want line: ${line}`);
|
|
89
|
+
}
|
|
90
|
+
wants.push(sha);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`upload-pack: unexpected pre-flush line: ${line}`);
|
|
94
|
+
}
|
|
95
|
+
if (line.startsWith("have ")) {
|
|
96
|
+
const sha = line.slice("have ".length).split(" ", 1)[0];
|
|
97
|
+
if (sha === undefined || sha.length === 0) {
|
|
98
|
+
throw new Error(`upload-pack: malformed have line: ${line}`);
|
|
99
|
+
}
|
|
100
|
+
haves.push(sha);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (line === "done") {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { wants, haves };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Per-commit cache entry built during the allowed-tip walk: the
|
|
112
|
+
* objects reachable from a given commit (tree + blobs + the commit
|
|
113
|
+
* itself) and the commit's parent SHAs. The cache lets the downstream
|
|
114
|
+
* want walk compute reachable-from-wants without re-reading commits
|
|
115
|
+
* or re-walking their trees.
|
|
116
|
+
*/
|
|
117
|
+
type CommitIndexEntry = {
|
|
118
|
+
readonly objects: ReadonlySet<string>;
|
|
119
|
+
readonly parents: readonly string[];
|
|
120
|
+
};
|
|
121
|
+
type CommitIndex = ReadonlyMap<string, CommitIndexEntry>;
|
|
122
|
+
|
|
123
|
+
async function walkAllowedAndIndex(
|
|
124
|
+
dir: string,
|
|
125
|
+
allowedTipShas: readonly string[],
|
|
126
|
+
): Promise<{ allowed: Set<string>; perCommit: CommitIndex }> {
|
|
127
|
+
const allowed = new Set<string>();
|
|
128
|
+
const perCommit = new Map<string, CommitIndexEntry>();
|
|
129
|
+
const seenCommits = new Set<string>();
|
|
130
|
+
const queue: string[] = [...allowedTipShas];
|
|
131
|
+
while (queue.length > 0) {
|
|
132
|
+
const oid = queue.shift();
|
|
133
|
+
if (oid === undefined) break;
|
|
134
|
+
if (seenCommits.has(oid)) continue;
|
|
135
|
+
seenCommits.add(oid);
|
|
136
|
+
let commit;
|
|
137
|
+
try {
|
|
138
|
+
commit = await git.readCommit({ fs, dir, oid });
|
|
139
|
+
} catch {
|
|
140
|
+
// Tip SHA points at something that no longer reads as a commit;
|
|
141
|
+
// skip rather than fail the whole walk.
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const objects = await collectReachableObjects(dir, oid);
|
|
145
|
+
const objectSet = new Set(objects);
|
|
146
|
+
perCommit.set(oid, { objects: objectSet, parents: commit.commit.parent });
|
|
147
|
+
for (const o of objects) allowed.add(o);
|
|
148
|
+
for (const p of commit.commit.parent) {
|
|
149
|
+
if (!seenCommits.has(p)) queue.push(p);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { allowed, perCommit };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Compute reachable-from-`starts` in pure memory using the commit
|
|
157
|
+
* index built by `walkAllowedAndIndex`. Every commit reachable from
|
|
158
|
+
* an allowed tip is in the index; ancestors of a valid want are
|
|
159
|
+
* therefore present without any further `git.readCommit` calls.
|
|
160
|
+
*
|
|
161
|
+
* The caller is responsible for ensuring every start OID is a commit
|
|
162
|
+
* present in the index — `classifyWants` enforces both. A start OID
|
|
163
|
+
* missing from the index is silently skipped; if it slips past the
|
|
164
|
+
* classifier (a non-commit OID reachable from an allowed ref's tree,
|
|
165
|
+
* or any other contract violation upstream) the result will be the
|
|
166
|
+
* empty set rather than a thrown error.
|
|
167
|
+
*/
|
|
168
|
+
function reachableFromIndex(
|
|
169
|
+
starts: readonly string[],
|
|
170
|
+
perCommit: CommitIndex,
|
|
171
|
+
): Set<string> {
|
|
172
|
+
const reachable = new Set<string>();
|
|
173
|
+
const seen = new Set<string>();
|
|
174
|
+
const queue: string[] = [...starts];
|
|
175
|
+
while (queue.length > 0) {
|
|
176
|
+
const oid = queue.shift();
|
|
177
|
+
if (oid === undefined) break;
|
|
178
|
+
if (seen.has(oid)) continue;
|
|
179
|
+
seen.add(oid);
|
|
180
|
+
const entry = perCommit.get(oid);
|
|
181
|
+
if (entry === undefined) continue;
|
|
182
|
+
for (const o of entry.objects) reachable.add(o);
|
|
183
|
+
for (const p of entry.parents) {
|
|
184
|
+
if (!seen.has(p)) queue.push(p);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return reachable;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function shaExistsAsCommit(dir: string, sha: string): Promise<boolean> {
|
|
191
|
+
try {
|
|
192
|
+
await git.readCommit({ fs, dir, oid: sha });
|
|
193
|
+
return true;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type WantClassification =
|
|
200
|
+
| { kind: "ok" }
|
|
201
|
+
| { kind: "forbidden" }
|
|
202
|
+
| { kind: "unknown" };
|
|
203
|
+
|
|
204
|
+
async function classifyWants(
|
|
205
|
+
dir: string,
|
|
206
|
+
wants: readonly string[],
|
|
207
|
+
allowedObjects: ReadonlySet<string>,
|
|
208
|
+
): Promise<WantClassification> {
|
|
209
|
+
// Two-pass classification with a stable preference: `forbidden`
|
|
210
|
+
// outranks `unknown` so the client always sees the same error
|
|
211
|
+
// vocabulary regardless of how it ordered its want lines. A SHA
|
|
212
|
+
// that exists but is reachable only from refs the token cannot see
|
|
213
|
+
// is more useful diagnostic information than a SHA we cannot find
|
|
214
|
+
// at all, and incident triage benefits from determinism.
|
|
215
|
+
//
|
|
216
|
+
// Membership in `allowedObjects` is necessary but not sufficient:
|
|
217
|
+
// the substrate's reachable-objects walk includes commits, trees,
|
|
218
|
+
// and blobs alike, so a hand-crafted client could otherwise want a
|
|
219
|
+
// blob OID that appears in some allowed ref's tree. This handler
|
|
220
|
+
// expects `want` lines to name commits — annotated-tag wants are
|
|
221
|
+
// not supported here today; a non-commit want is classified the
|
|
222
|
+
// same as a SHA that does not exist at all.
|
|
223
|
+
let sawForbidden = false;
|
|
224
|
+
let sawUnknown = false;
|
|
225
|
+
for (const want of wants) {
|
|
226
|
+
if (allowedObjects.has(want)) {
|
|
227
|
+
if (await shaExistsAsCommit(dir, want)) continue;
|
|
228
|
+
sawUnknown = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const exists = await shaExistsAsCommit(dir, want);
|
|
232
|
+
if (exists) {
|
|
233
|
+
sawForbidden = true;
|
|
234
|
+
} else {
|
|
235
|
+
sawUnknown = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (sawForbidden) return { kind: "forbidden" };
|
|
239
|
+
if (sawUnknown) return { kind: "unknown" };
|
|
240
|
+
return { kind: "ok" };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function errorResponse(msg: string): Response {
|
|
244
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
245
|
+
async start(controller) {
|
|
246
|
+
const sink = new WritableStream<Uint8Array>({
|
|
247
|
+
write(chunk) {
|
|
248
|
+
controller.enqueue(chunk);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const writer = sink.getWriter();
|
|
252
|
+
try {
|
|
253
|
+
await writeErr(writer, msg);
|
|
254
|
+
await writer.close();
|
|
255
|
+
controller.close();
|
|
256
|
+
} catch (cause) {
|
|
257
|
+
await writer.abort(cause).catch(() => undefined);
|
|
258
|
+
controller.error(cause);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
return new Response(stream, {
|
|
263
|
+
status: 200,
|
|
264
|
+
headers: { "content-type": UPLOAD_PACK_RESULT_CONTENT_TYPE },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function packToReadable(pack: Uint8Array): ReadableStream<Uint8Array> {
|
|
269
|
+
return new ReadableStream<Uint8Array>({
|
|
270
|
+
start(controller) {
|
|
271
|
+
if (pack.length > 0) controller.enqueue(pack);
|
|
272
|
+
controller.close();
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function successResponse(pack: Uint8Array | null): Response {
|
|
278
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
279
|
+
async start(controller) {
|
|
280
|
+
const sink = new WritableStream<Uint8Array>({
|
|
281
|
+
write(chunk) {
|
|
282
|
+
controller.enqueue(chunk);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const writer = sink.getWriter();
|
|
286
|
+
try {
|
|
287
|
+
await writePktLine(writer, "NAK\n");
|
|
288
|
+
} catch (cause) {
|
|
289
|
+
await writer.abort(cause).catch(() => undefined);
|
|
290
|
+
controller.error(cause);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (pack !== null && pack.length > 0) {
|
|
294
|
+
const sideBand = chunkPackToSideBand(packToReadable(pack));
|
|
295
|
+
const reader = sideBand.getReader();
|
|
296
|
+
try {
|
|
297
|
+
for (;;) {
|
|
298
|
+
const r = await reader.read();
|
|
299
|
+
if (r.done) break;
|
|
300
|
+
if (r.value) controller.enqueue(r.value);
|
|
301
|
+
}
|
|
302
|
+
} catch (cause) {
|
|
303
|
+
await writer.abort(cause).catch(() => undefined);
|
|
304
|
+
controller.error(cause);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Per the smart-HTTP transfer spec, the upload-pack response
|
|
309
|
+
// terminates with a flush pkt-line after the side-band stream.
|
|
310
|
+
// Without this stock git aborts with `unexpected disconnect
|
|
311
|
+
// while reading sideband packet`.
|
|
312
|
+
try {
|
|
313
|
+
await writeFlush(writer);
|
|
314
|
+
await writer.close();
|
|
315
|
+
controller.close();
|
|
316
|
+
} catch (cause) {
|
|
317
|
+
await writer.abort(cause).catch(() => undefined);
|
|
318
|
+
controller.error(cause);
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
return new Response(stream, {
|
|
323
|
+
status: 200,
|
|
324
|
+
headers: { "content-type": UPLOAD_PACK_RESULT_CONTENT_TYPE },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Translate substrate-thrown errors that escape the listRefs /
|
|
330
|
+
* getRepoDir / pack-build call chain into the upload-pack pkt-line
|
|
331
|
+
* ERR vocabulary. Only `authorize_denied:` is reachable from the
|
|
332
|
+
* upload-pack call chain; the receive-pack prefixes belong to
|
|
333
|
+
* `receive-pack.ts`'s translator. Upload-pack has no per-ref status
|
|
334
|
+
* channel, so the substrate's authorize denial collapses into the
|
|
335
|
+
* same `ERR forbidden ref` shape used for refPattern denial.
|
|
336
|
+
*
|
|
337
|
+
* Returns `null` when the error message does not carry the known
|
|
338
|
+
* substrate prefix; the caller rethrows in that case so a genuine
|
|
339
|
+
* crash still bubbles to the HTTP layer as a 500.
|
|
340
|
+
*/
|
|
341
|
+
function translateSubstrateError(err: unknown): Response | null {
|
|
342
|
+
if (!(err instanceof Error)) return null;
|
|
343
|
+
if (err.message.startsWith("authorize_denied:")) {
|
|
344
|
+
return errorResponse("forbidden ref");
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function handleUploadPack(
|
|
350
|
+
repoStore: UploadPackRepoStore,
|
|
351
|
+
principal: UploadPackPrincipal,
|
|
352
|
+
repoId: RepoId,
|
|
353
|
+
request: Request,
|
|
354
|
+
): Promise<Response> {
|
|
355
|
+
const bodyBuf = new Uint8Array(await request.arrayBuffer());
|
|
356
|
+
const { wants, haves } = await parseUploadRequest(bodyBuf);
|
|
357
|
+
if (wants.length === 0) {
|
|
358
|
+
return errorResponse("upload-pack: no want lines");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const dir = await repoStore.getRepoDir(principal, repoId);
|
|
363
|
+
const allRefs = await repoStore.listRefs(principal, repoId);
|
|
364
|
+
const refPattern = principal.tokenClaims.refPattern;
|
|
365
|
+
const allowedTips = allRefs
|
|
366
|
+
.filter((r) => glob.match(refPattern, r.name))
|
|
367
|
+
.map((r) => r.sha);
|
|
368
|
+
|
|
369
|
+
const { allowed: allowedObjects, perCommit } = await walkAllowedAndIndex(
|
|
370
|
+
dir,
|
|
371
|
+
allowedTips,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const classification = await classifyWants(dir, wants, allowedObjects);
|
|
375
|
+
if (classification.kind === "forbidden") {
|
|
376
|
+
return errorResponse("forbidden ref");
|
|
377
|
+
}
|
|
378
|
+
if (classification.kind === "unknown") {
|
|
379
|
+
return errorResponse("upload-pack: not our ref");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const wantedObjects = reachableFromIndex(wants, perCommit);
|
|
383
|
+
const result = await createNegotiatedPack(
|
|
384
|
+
dir,
|
|
385
|
+
wants,
|
|
386
|
+
haves,
|
|
387
|
+
(oid) => allowedObjects.has(oid),
|
|
388
|
+
{ wantedObjects },
|
|
389
|
+
);
|
|
390
|
+
return successResponse(result === null ? null : result.pack);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const translated = translateSubstrateError(err);
|
|
393
|
+
if (translated !== null) return translated;
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { createAuth, type Auth } from "./auth";
|
|
2
|
+
export {
|
|
3
|
+
createApp,
|
|
4
|
+
createHubContextMiddleware,
|
|
5
|
+
mountHubRoutes,
|
|
6
|
+
type App,
|
|
7
|
+
type CreateAppOpts,
|
|
8
|
+
type CreateHubContextMiddlewareDeps,
|
|
9
|
+
type MountHubRoutesDeps,
|
|
10
|
+
} from "./app";
|
|
11
|
+
export {
|
|
12
|
+
createRequireGrant,
|
|
13
|
+
idResource,
|
|
14
|
+
type CreateRequireGrantDeps,
|
|
15
|
+
type RequireGrant,
|
|
16
|
+
} from "./middleware/grant";
|
|
17
|
+
export {
|
|
18
|
+
createResolveTenant,
|
|
19
|
+
requireAuth,
|
|
20
|
+
type CreateResolveTenantDeps,
|
|
21
|
+
} from "./middleware/tenant";
|
|
22
|
+
export type { AppEnv, TenantEnv, TenantRow, PrincipalRow } from "./context";
|
|
23
|
+
export type { GetSession, SessionInfo, SessionUser } from "./session";
|