@intx/hub-sessions 0.1.2 → 0.2.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.
Files changed (87) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +84 -1
  3. package/dist/agent-repo.d.ts +89 -0
  4. package/dist/agent-repo.js +109 -0
  5. package/dist/agent-state-kind.d.ts +12 -0
  6. package/dist/agent-state-kind.js +185 -0
  7. package/dist/asset-service.d.ts +123 -0
  8. package/dist/asset-service.js +349 -0
  9. package/dist/available-skills-stanza.d.ts +21 -0
  10. package/dist/available-skills-stanza.js +32 -0
  11. package/dist/credential-push.d.ts +32 -0
  12. package/dist/credential-push.js +85 -0
  13. package/dist/event-collector-registry.d.ts +20 -0
  14. package/dist/event-collector-registry.js +115 -0
  15. package/dist/event-collector.d.ts +39 -0
  16. package/dist/event-collector.js +357 -0
  17. package/dist/hub-session-lookups.d.ts +17 -0
  18. package/dist/hub-session-lookups.js +204 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +122 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +16 -0
  23. package/dist/package-registry-kind.d.ts +70 -0
  24. package/dist/package-registry-kind.js +260 -0
  25. package/dist/repo-store/index.d.ts +4 -0
  26. package/dist/repo-store/index.js +3 -0
  27. package/dist/repo-store/store.d.ts +41 -0
  28. package/dist/repo-store/store.js +1692 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +53 -0
  30. package/dist/repo-store/subscribe-kind.js +179 -0
  31. package/dist/repo-store/types.d.ts +483 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +235 -0
  34. package/dist/session-service.js +997 -0
  35. package/dist/skill-kind.d.ts +41 -0
  36. package/dist/skill-kind.js +288 -0
  37. package/dist/substrate.d.ts +8 -0
  38. package/dist/substrate.js +21 -0
  39. package/dist/workflow-kind.d.ts +21 -0
  40. package/dist/workflow-kind.js +263 -0
  41. package/dist/workflow-run-event-log.d.ts +21 -0
  42. package/dist/workflow-run-event-log.js +51 -0
  43. package/dist/workflow-run-kind.d.ts +326 -0
  44. package/dist/workflow-run-kind.js +2646 -0
  45. package/dist/workflow-run-reader.d.ts +47 -0
  46. package/dist/workflow-run-reader.js +157 -0
  47. package/dist/ws/index.d.ts +3 -0
  48. package/dist/ws/index.js +3 -0
  49. package/dist/ws/sidecar-events.d.ts +134 -0
  50. package/dist/ws/sidecar-events.js +70 -0
  51. package/dist/ws/sidecar-handler.d.ts +184 -0
  52. package/dist/ws/sidecar-handler.js +1603 -0
  53. package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
  54. package/dist/ws/sidecar-token-authenticator.js +24 -0
  55. package/package.json +34 -12
  56. package/src/agent-repo.test.ts +0 -310
  57. package/src/agent-repo.ts +0 -165
  58. package/src/agent-state-kind.test.ts +0 -247
  59. package/src/agent-state-kind.ts +0 -204
  60. package/src/asset-service.test.ts +0 -540
  61. package/src/asset-service.ts +0 -378
  62. package/src/available-skills-stanza.test.ts +0 -87
  63. package/src/available-skills-stanza.ts +0 -47
  64. package/src/credential-push.ts +0 -65
  65. package/src/event-collector-registry.test.ts +0 -73
  66. package/src/event-collector-registry.ts +0 -171
  67. package/src/event-collector.test.ts +0 -1387
  68. package/src/event-collector.ts +0 -424
  69. package/src/hub-session-lookups.ts +0 -206
  70. package/src/hub-session-orchestrator.test.ts +0 -510
  71. package/src/hub-session-orchestrator.ts +0 -213
  72. package/src/index.ts +0 -78
  73. package/src/repo-store/index.ts +0 -15
  74. package/src/repo-store/store.test.ts +0 -1169
  75. package/src/repo-store/store.ts +0 -428
  76. package/src/repo-store/types.ts +0 -253
  77. package/src/session-service.test.ts +0 -895
  78. package/src/session-service.ts +0 -464
  79. package/src/skill-kind.test.ts +0 -599
  80. package/src/skill-kind.ts +0 -350
  81. package/src/ws/index.ts +0 -18
  82. package/src/ws/sidecar-events.test.ts +0 -96
  83. package/src/ws/sidecar-events.ts +0 -231
  84. package/src/ws/sidecar-handler.test.ts +0 -2217
  85. package/src/ws/sidecar-handler.ts +0 -1574
  86. package/tsconfig.json +0 -4
  87. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,483 @@
1
+ import type { RepoKind, RepoId, RepoAction } from "@intx/types/sidecar";
2
+ export type { RepoKind, RepoId, RepoAction };
3
+ /**
4
+ * arktype validator for the `user` principal variant. The substrate
5
+ * only requires the `kind` discriminant; kind handlers that accept
6
+ * user-token-authenticated requests rely on this shape to
7
+ * cross-check the pre-resolved authz verdict against the bearer
8
+ * token's claims. The validator is exported alongside the type so
9
+ * handlers can call it for a structural narrow without re-declaring
10
+ * the shape.
11
+ *
12
+ * Field semantics:
13
+ * - `authz`: the pre-resolved grant verdict from the route layer.
14
+ * The kind handler does NOT re-query the grant store; it only
15
+ * sanity-checks that the verdict targets the right resource and
16
+ * grant verb, then defers to `effect`.
17
+ * - `tokenClaims`: the bearer-token's scope. The kind handler
18
+ * verifies the requested `(ref, action)` falls inside this scope
19
+ * synchronously (`actions.includes(action)`,
20
+ * `glob.match(refPattern, ref)`, `Date.now() < expiresAt`).
21
+ */
22
+ export declare const UserPrincipal: import("arktype/internal/variants/object.ts").ObjectType<{
23
+ kind: "user";
24
+ principalId: string;
25
+ tenantId: string;
26
+ authz: {
27
+ effect: "allow" | "deny";
28
+ resource: string;
29
+ grantVerb: string;
30
+ };
31
+ tokenClaims: {
32
+ refPattern: string;
33
+ actions: ("init" | "writeTree" | "receivePack" | "createPack" | "resolveRef")[];
34
+ expiresAt: number;
35
+ };
36
+ }, {}>;
37
+ export type UserPrincipal = typeof UserPrincipal.infer;
38
+ /**
39
+ * Regex defining the shape of a valid `RepoId.id`. The substrate
40
+ * validates against this at every public operation and throws an Error
41
+ * prefixed with `"repo_id_invalid: "` on mismatch.
42
+ */
43
+ export declare const SAFE_REPO_ID: RegExp;
44
+ /**
45
+ * Principal is a discriminated-union extension point. The substrate
46
+ * requires only the `kind` discriminant; concrete principal shapes live
47
+ * in kind-handler packages and are narrowed via `principal.kind === "..."`
48
+ * checks plus arktype validation in the handler. No index signature is
49
+ * declared here so that handlers do not need `as Type` casts to access
50
+ * their own fields.
51
+ */
52
+ export type Principal = {
53
+ readonly kind: string;
54
+ };
55
+ /**
56
+ * Authorization callback supplied to the repo-store. Called once per
57
+ * substrate operation that requires gating (`writeTree`, `receivePack`,
58
+ * `createPack`, `resolveRef`, plus the bulk-read variants below). The
59
+ * substrate translates an `allowed: false` verdict into a thrown Error
60
+ * prefixed with `"authorize_denied: "` carrying the supplied reason.
61
+ *
62
+ * The substrate passes the literal string `"*"` as `ref` when it calls
63
+ * the authorize hook on behalf of the bulk-read methods `listRefs` and
64
+ * `resolveHead`. Both methods enumerate refs across the whole repo and
65
+ * have no single ref to feed into a per-ref claim check; the sentinel
66
+ * lets kind handlers recognise the call and skip the per-ref
67
+ * `refPattern` match while still gating on `action` (always
68
+ * `"resolveRef"` for the bulk case) and expiry. Per-ref refPattern
69
+ * filtering for the response payload is the responsibility of the
70
+ * caller (the advertise-refs layer), not the authorize hook.
71
+ */
72
+ export type AuthorizeFn = (principal: Principal, repoId: RepoId, ref: string, action: RepoAction) => {
73
+ allowed: true;
74
+ } | {
75
+ allowed: false;
76
+ reason: string;
77
+ };
78
+ /**
79
+ * A run whose terminal event (e.g. `RunCompleted`) is *newly added* by the
80
+ * commit under validation -- present in the prospective tree and absent from
81
+ * the prior tree. The kind handler authoritatively detects this during its
82
+ * validation walk and surfaces it so callers do not re-derive terminal-ness
83
+ * by sniffing committed path shapes. `terminalEventJson` carries the raw bytes
84
+ * of the terminal event blob so a caller can reconstruct the event without a
85
+ * second read. A commit that carries an already-terminal run forward
86
+ * unchanged (e.g. a later compaction commit) is NOT newly terminal and does
87
+ * not appear here.
88
+ */
89
+ export type NewlyTerminalRun = {
90
+ runId: string;
91
+ terminalEventJson: string;
92
+ };
93
+ export type ValidatePushResult = {
94
+ ok: true;
95
+ newlyTerminalRuns?: NewlyTerminalRun[];
96
+ } | {
97
+ ok: false;
98
+ reason: string;
99
+ };
100
+ /**
101
+ * Result of a `writeTree` / `writeTreePreservingPrefix` commit. `commitSha` is
102
+ * the new commit. `newlyTerminalRuns` surfaces the kind handler's terminal
103
+ * detection (empty for handlers and commits that produce none) so callers can
104
+ * react to a run reaching a terminal event without re-deriving it from the
105
+ * committed path shape.
106
+ */
107
+ export type WriteResult = {
108
+ commitSha: string;
109
+ newlyTerminalRuns: readonly NewlyTerminalRun[];
110
+ };
111
+ /**
112
+ * Per-call options for `initRepo`. Currently a single override —
113
+ * `gitignore` — that overrides the body written to `.gitignore` in
114
+ * the genesis tree. When omitted, the substrate's default body is
115
+ * used. The asset REST handler supplies a richer body that includes
116
+ * OS/editor cruft, common build output, and `keys/`.
117
+ */
118
+ export type InitRepoOpts = {
119
+ gitignore?: string;
120
+ };
121
+ /**
122
+ * A single ref entry returned by `RepoStore.listRefs`. `name` is the
123
+ * fully-qualified ref name (`refs/heads/main`, `refs/tags/v1`, ...);
124
+ * `sha` is the SHA-1 the ref currently resolves to.
125
+ */
126
+ export type RefEntry = {
127
+ readonly name: string;
128
+ readonly sha: string;
129
+ };
130
+ export type TreeContent = {
131
+ /**
132
+ * Map of repo-relative path to file contents. Each entry is written
133
+ * to the working tree and staged before commit.
134
+ */
135
+ files: Record<string, string | Uint8Array>;
136
+ /**
137
+ * Optional directory-subtree prefix to clear before staging. When
138
+ * set, every tracked path beginning with this prefix is removed
139
+ * from the git index and the corresponding directory on disk is
140
+ * deleted before `files` is written. Must end with `/` and must
141
+ * not contain `..` or absolute path components. When unset,
142
+ * writeTree is purely additive.
143
+ */
144
+ clearPrefix?: string;
145
+ /** Commit message for the resulting commit. */
146
+ message: string;
147
+ };
148
+ /**
149
+ * Per-call options for `RepoStore.writeTreePreservingPrefix`. The
150
+ * substrate enumerates existing blobs under `preservePrefix` from the
151
+ * current ref tip while holding the per-repo lock, calls the caller's
152
+ * `merge` callback with those entries, and writes the returned set as
153
+ * the new value of the prefix subtree. Two concurrent callers
154
+ * targeting the same prefix serialize at the lock so neither one
155
+ * observes a stale pre-image of the other's commit.
156
+ */
157
+ export type WriteTreePreservingPrefixArgs = {
158
+ /**
159
+ * Directory-subtree prefix whose existing blobs are surfaced to
160
+ * `merge` and then replaced wholesale by its return value. Must end
161
+ * with `/` and must not contain `..` or absolute path components.
162
+ */
163
+ preservePrefix: string;
164
+ /**
165
+ * Called under the per-repo lock with the current set of blobs
166
+ * directly under `preservePrefix` (keyed by repo-root-relative
167
+ * path, including the prefix). Returns the full set of files the
168
+ * substrate should write at the prefix; the prefix subtree is
169
+ * cleared and replaced with this set in a single commit. Paths
170
+ * outside the prefix are passed through unchanged. The callback may
171
+ * throw to abort the write; the substrate releases the lock and
172
+ * propagates the error.
173
+ */
174
+ merge: (existing: ReadonlyMap<string, Uint8Array>) => Promise<Record<string, string | Uint8Array>>;
175
+ /** Commit message for the resulting commit. */
176
+ message: string;
177
+ };
178
+ /**
179
+ * Per-call options for `RepoStore.writeTreeDelta`. `computeDelta` runs
180
+ * under the per-repo lock against the pinned parent tip (`parentCommitSha`,
181
+ * null when the ref does not yet exist) and returns the exact files to
182
+ * write and paths to delete; the substrate carries every other entry
183
+ * forward by its object id. A delete ending in `/` clears that subtree;
184
+ * any other delete clears the single file at that path. A delete whose
185
+ * base entry is the wrong type -- a no-slash delete naming a directory,
186
+ * or a trailing-slash delete descending into a file -- is rejected.
187
+ *
188
+ * `changedPathPrefixes` is the validation scoping hint for the touched
189
+ * region (e.g. `addresses/<seg>/`), supplied by the caller because a
190
+ * delta has no single clear-prefix to derive it from; `undefined` means
191
+ * validate the whole tree.
192
+ *
193
+ * The `prior` argument exposes cache-backed reads of that pinned parent
194
+ * tree -- `listDirOids` for a directory's `{name, oid}` children and
195
+ * `readBlobByOid` for a blob by its object id -- so the callback reads
196
+ * through the store's per-repo object cache under the same lock rather
197
+ * than re-opening the repo.
198
+ */
199
+ export type PriorDeltaReads = {
200
+ readBlobByOid: (oid: string) => Promise<Uint8Array>;
201
+ listDirOids: (path: string) => Promise<{
202
+ name: string;
203
+ oid: string;
204
+ }[]>;
205
+ };
206
+ export type WriteTreeDeltaArgs = {
207
+ computeDelta: (parentCommitSha: string | null, prior: PriorDeltaReads) => Promise<{
208
+ puts: Record<string, string | Uint8Array>;
209
+ deletes: readonly string[];
210
+ }>;
211
+ changedPathPrefixes: ReadonlySet<string> | undefined;
212
+ message: string;
213
+ };
214
+ export interface KindHandler {
215
+ kind: RepoKind;
216
+ /**
217
+ * On-disk directory under `dataDir` for repos of this kind. Allows
218
+ * each kind to declare its own layout (e.g. "agents") so the
219
+ * substrate does not hard-code a `<kind>/<id>` path.
220
+ */
221
+ directoryPrefix: string;
222
+ /**
223
+ * Inspect the prospective commit's tree before the ref is
224
+ * advanced. Return `{ ok: false, reason }` to reject the write.
225
+ * The substrate translates rejection into a thrown Error whose
226
+ * message begins with `"path_violation: "`.
227
+ *
228
+ * Runs on every `receivePack` and every `writeTree` independently
229
+ * of the authorize verdict: authorize gates access, validatePush
230
+ * enforces content rules.
231
+ *
232
+ * `topLevelTreePaths` lists the names directly under the prospective
233
+ * tree root. `readBlob` reads any blob in the prospective tree by
234
+ * repo-root-relative POSIX path (e.g. `greet/SKILL.md`). `listDir`
235
+ * enumerates the names directly under a tree-root-relative POSIX
236
+ * directory path (no trailing slash, no leading slash); pass the
237
+ * empty string to list the root. Handlers that only need path-level
238
+ * checks can ignore `readBlob` and `listDir`.
239
+ *
240
+ * `priorReadBlob` and `priorListDir` mirror `readBlob` / `listDir`
241
+ * against the parent commit's tree — the ref's tip at the moment
242
+ * validatePush runs. Handlers use these to compare prospective
243
+ * content against the immediately-prior bytes (e.g. enforcing
244
+ * append-only invariants by rejecting any path whose prior bytes
245
+ * differ from the prospective bytes). `priorReadBlob` returns
246
+ * `null` when the path did not exist at the prior tree (or the
247
+ * ref has no prior commit — first push). `priorListDir` returns
248
+ * an empty array in the same cases.
249
+ *
250
+ * `principal` is the principal performing the push, the same value
251
+ * fed to the `authorize` hook. Handlers use it for principal-vs-
252
+ * payload cross-checks that a structural shape validator cannot
253
+ * express (e.g. "only a `hub` principal may write a `CancelRequested`
254
+ * whose origin is `hub-admin`").
255
+ *
256
+ * `changedPathPrefixes` is the set of repo-root-relative POSIX path
257
+ * prefixes (each ending in `/`) under which this commit could have
258
+ * mutated tree entries -- the cleared prefix for a `writeTree`
259
+ * carrying a `clearPrefix`, or the subtrees whose object differs from
260
+ * the parent commit for a received pack. Every path the commit can
261
+ * have changed relative to its parent is under one of these prefixes;
262
+ * any path outside them is carried forward byte-identical by the
263
+ * substrate. It is `undefined` when the substrate cannot bound the
264
+ * change set (no parent to diff against and no `clearPrefix`), in
265
+ * which case the handler must validate the whole prospective tree. A
266
+ * handler with per-subtree invariants that cannot be affected by a
267
+ * commit outside that subtree (workflow-run's per-run append-only
268
+ * log) uses this to skip re-validating subtrees the commit provably
269
+ * did not touch; a handler with no such structure ignores it and
270
+ * validates unconditionally.
271
+ *
272
+ * `priorListDirOids` mirrors `priorListDir` but returns each child
273
+ * entry's git object id alongside its name, read straight from the
274
+ * prior commit's tree (git trees are content-addressed, so the listing
275
+ * already carries the OID). A handler that validates a large retained
276
+ * subtree by its per-commit delta uses it to prove a retained entry is
277
+ * byte-unchanged by OID equality instead of re-reading the blob. It is
278
+ * `undefined` when no prior commit exists.
279
+ *
280
+ * `listDirOids` is the prospective-side mirror of `priorListDirOids`:
281
+ * each child entry's OID read straight from the prospective tree's
282
+ * listing, so a handler comparing a retained subtree by OID gets the
283
+ * prospective OID without re-reading and hashing every entry's bytes.
284
+ * It is `undefined` on paths that do not surface it (a hand-built
285
+ * validatePush in a test), in which case the handler falls back to
286
+ * hashing the prospective bytes.
287
+ */
288
+ validatePush: (args: {
289
+ repoId: RepoId;
290
+ ref: string;
291
+ principal: Principal;
292
+ topLevelTreePaths: string[];
293
+ readBlob: (path: string) => Promise<Uint8Array>;
294
+ listDir: (path: string) => Promise<string[]>;
295
+ listDirOids?: (path: string) => Promise<{
296
+ name: string;
297
+ oid: string;
298
+ }[]>;
299
+ priorReadBlob: (path: string) => Promise<Uint8Array | null>;
300
+ priorListDir: (path: string) => Promise<string[]>;
301
+ priorListDirOids?: (path: string) => Promise<{
302
+ name: string;
303
+ oid: string;
304
+ }[]>;
305
+ changedPathPrefixes?: ReadonlySet<string> | undefined;
306
+ }) => Promise<ValidatePushResult> | ValidatePushResult;
307
+ /**
308
+ * Fired after a successful ref update from any operation. `oldSha`
309
+ * is `null` when the ref did not exist before the update.
310
+ */
311
+ onRefUpdated: (args: {
312
+ repoId: RepoId;
313
+ ref: string;
314
+ oldSha: string | null;
315
+ newSha: string;
316
+ }) => Promise<void> | void;
317
+ }
318
+ export interface RepoStore {
319
+ /**
320
+ * Bookkeeping primitive. Idempotent. Creates the repo directory
321
+ * and initializes git when not already present. Not gated by
322
+ * `authorize`: the only state it can produce is an empty repo, so
323
+ * the higher-level question of who may mint a new `<kind>/<id>`
324
+ * lives at the caller. The substrate also calls `initRepo`
325
+ * internally from `writeTree` and `receivePack`, so first-touch
326
+ * operations succeed without an explicit init call.
327
+ */
328
+ initRepo(repoId: RepoId, opts?: InitRepoOpts): Promise<void>;
329
+ writeTree(principal: Principal, repoId: RepoId, ref: string, content: TreeContent): Promise<WriteResult>;
330
+ /**
331
+ * Read-then-write variant for use cases that mutate a single
332
+ * directory subtree against its current contents (overwrite one
333
+ * entry, delete one entry, augment by one entry). The substrate
334
+ * enumerates blobs under `args.preservePrefix` while holding the
335
+ * per-repo lock, invokes `args.merge` with those entries, and
336
+ * commits the returned set as the new value of the prefix.
337
+ *
338
+ * Two concurrent callers targeting the same prefix serialize at the
339
+ * lock, so the merge callback's pre-image is always the previous
340
+ * commit's tip — there is no read-outside-the-lock window where one
341
+ * caller could base its write on a stale view of the prefix.
342
+ *
343
+ * The substrate handles `clearPrefix` and the commit internally;
344
+ * paths outside the prefix are untouched.
345
+ */
346
+ writeTreePreservingPrefix(principal: Principal, repoId: RepoId, ref: string, args: WriteTreePreservingPrefixArgs): Promise<WriteResult>;
347
+ /**
348
+ * Delta variant for mutating a few named entries in a large subtree
349
+ * without re-materializing the untouched siblings. `computeDelta` runs
350
+ * under the per-repo lock against the pinned parent tip and returns the
351
+ * files to put and the paths to delete; everything else is carried
352
+ * forward by object id. Use this over `writeTreePreservingPrefix` when
353
+ * the untouched remainder of the prefix is large (e.g. a claim-check
354
+ * move that adds one entry and deletes another while the consumed dedup
355
+ * index carries forward unchanged).
356
+ */
357
+ writeTreeDelta(principal: Principal, repoId: RepoId, ref: string, args: WriteTreeDeltaArgs): Promise<WriteResult>;
358
+ /**
359
+ * Receive a packfile and advance `ref` to `commitSha`.
360
+ *
361
+ * `expectedOldSha` is a compare-and-set guard the substrate runs
362
+ * under the per-repo lock. Pass a SHA string to require the ref
363
+ * currently points there; pass `null` to require the ref does not
364
+ * yet exist. On mismatch the call throws with a `non_fast_forward:`
365
+ * prefix and leaves the ref untouched.
366
+ *
367
+ * Callers that do not have the old SHA in hand should resolve it
368
+ * via `resolveRef` first; the substrate exposes no force-write
369
+ * mode because silently overwriting a losing concurrent update is
370
+ * never the right behavior.
371
+ */
372
+ receivePack(principal: Principal, repoId: RepoId, ref: string, pack: Uint8Array, commitSha: string, expectedOldSha: string | null): Promise<void>;
373
+ createPack(principal: Principal, repoId: RepoId, ref: string): Promise<{
374
+ pack: Uint8Array;
375
+ commitSha: string;
376
+ ref: string;
377
+ }>;
378
+ /**
379
+ * Commit the "last shipped tip" for an incremental (`workflow-run`)
380
+ * pack ref, advancing the cursor `createPack` walks back to. The
381
+ * caller invokes this only once the receiver has acked the transfer
382
+ * that shipped `commitSha`, so an incremental pack the receiver never
383
+ * acknowledged — a transfer a reconnect cancelled before its ack —
384
+ * leaves the cursor where it was and the next `createPack` re-ships
385
+ * the un-acked commits. Building a pack must NOT advance the cursor
386
+ * itself: a built-but-un-acked transfer would then strand the
387
+ * receiver with a chain whose base commit the pack omits. A no-op for
388
+ * kinds whose packs are not incremental.
389
+ */
390
+ commitPackedTip(repoId: RepoId, ref: string, commitSha: string): void;
391
+ resolveRef(principal: Principal, repoId: RepoId, ref: string): Promise<string | null>;
392
+ /**
393
+ * Enumerate the repo's refs (branches and tags), lexicographically
394
+ * sorted by name. The principal is gated under the same
395
+ * `resolveRef` action that `resolveRef` itself enforces — the
396
+ * substrate does not duplicate the check on a per-ref basis. When
397
+ * the on-disk repo does not yet exist (the bookkeeping primitive
398
+ * `initRepo` has never been called), the result is the empty list.
399
+ */
400
+ listRefs(principal: Principal, repoId: RepoId): Promise<RefEntry[]>;
401
+ /**
402
+ * Resolve HEAD into the ref it symbolically points at plus the SHA
403
+ * that ref currently resolves to. The principal is gated under the
404
+ * same `resolveRef` action that `resolveRef` and `listRefs`
405
+ * enforce. Returns `null` when:
406
+ * - The on-disk repo does not yet exist (mirrors `listRefs`'s
407
+ * empty-list contract for uninitialised repos).
408
+ * - HEAD is detached (no symbolic target).
409
+ * - HEAD's symbolic target does not resolve (unborn ref).
410
+ * The smart-HTTP advertise layer uses the result to emit
411
+ * `symref=HEAD:<target>` so stock `git clone` lands on a real
412
+ * branch instead of leaving the working tree unborn.
413
+ */
414
+ resolveHead(principal: Principal, repoId: RepoId): Promise<{
415
+ symbolicTarget: string;
416
+ sha: string;
417
+ } | null>;
418
+ /**
419
+ * Synchronously return the on-disk directory backing the repo.
420
+ * The path is the result of composing the substrate's `dataDir`,
421
+ * the kind handler's `directoryPrefix`, and the validated
422
+ * `repoId.id`. This carries no authorize gate: it is a pure path
423
+ * computation. Consumers of the path (the smart-HTTP wire
424
+ * handlers) remain authorize-gated through the substrate methods
425
+ * they reach into for ref-listing and pack negotiation.
426
+ */
427
+ getRepoDir(repoId: RepoId): string;
428
+ /**
429
+ * Tail a ref's commit log. Returns an async iterator that emits
430
+ * `{ seq, event }` entries: one per commit on the ref. `seq` is
431
+ * zero-indexed at the ref's root commit and counts ancestors
432
+ * walking forward to HEAD, so the same commit always carries the
433
+ * same `seq` across restarts. The emitted `event` is the
434
+ * substrate-level commit descriptor; consumers that need richer
435
+ * shapes layer their own decoding on top.
436
+ *
437
+ * Cancellation: when `opts.signal` aborts, the iterator ends
438
+ * cleanly (no throw from the consumer's `for await`). The
439
+ * substrate releases the watcher slot on the same abort tick.
440
+ *
441
+ * Replay vs live:
442
+ * - `from: { seq: number }` enumerates every prior commit on the
443
+ * ref whose computed `seq` is >= the supplied number, then
444
+ * transitions to live mode and continues with new commits.
445
+ * - `from: "head"` records HEAD-of-ref at subscribe time and
446
+ * emits only commits that land strictly after.
447
+ *
448
+ * Backpressure: events are buffered in userspace bounded by
449
+ * `bufferLimit` (default 1024). On overrun the iterator throws a
450
+ * loud error; silent drop would corrupt audit. Consumers that
451
+ * cannot keep up are expected to abort.
452
+ *
453
+ * The substrate's vocabulary is the ref-update envelope. Consumers
454
+ * that need to filter on a richer event kind (e.g. a workflow-event
455
+ * `type` discriminator committed at the new ref) layer a decoder on
456
+ * top — see `subscribeKind` for the typed entrypoint that loads the
457
+ * committed payload, narrows it with an arktype validator, and
458
+ * applies a per-call kind filter.
459
+ */
460
+ subscribe(principal: Principal, repoId: RepoId, ref: string, opts: {
461
+ signal: AbortSignal;
462
+ from: "head" | {
463
+ seq: number;
464
+ };
465
+ bufferLimit?: number;
466
+ }): AsyncIterableIterator<{
467
+ seq: number;
468
+ event: unknown;
469
+ }>;
470
+ }
471
+ /**
472
+ * Substrate-level event shape emitted by `RepoStore.subscribe`.
473
+ * Each successful commit on a watched ref produces one event with
474
+ * this shape. The substrate is schema-agnostic; higher layers that
475
+ * want to surface richer event vocabularies build their own decoders
476
+ * on top (see `subscribeKind` for the workflow-event entrypoint).
477
+ */
478
+ export type RepoStoreSubscribeEvent = {
479
+ readonly type: "ref.updated";
480
+ readonly ref: string;
481
+ readonly oldSha: string | null;
482
+ readonly newSha: string;
483
+ };
@@ -0,0 +1,42 @@
1
+ import { type } from "arktype";
2
+ import { RepoAction as RepoActionSchema } from "@intx/types/sidecar";
3
+ /**
4
+ * arktype validator for the `user` principal variant. The substrate
5
+ * only requires the `kind` discriminant; kind handlers that accept
6
+ * user-token-authenticated requests rely on this shape to
7
+ * cross-check the pre-resolved authz verdict against the bearer
8
+ * token's claims. The validator is exported alongside the type so
9
+ * handlers can call it for a structural narrow without re-declaring
10
+ * the shape.
11
+ *
12
+ * Field semantics:
13
+ * - `authz`: the pre-resolved grant verdict from the route layer.
14
+ * The kind handler does NOT re-query the grant store; it only
15
+ * sanity-checks that the verdict targets the right resource and
16
+ * grant verb, then defers to `effect`.
17
+ * - `tokenClaims`: the bearer-token's scope. The kind handler
18
+ * verifies the requested `(ref, action)` falls inside this scope
19
+ * synchronously (`actions.includes(action)`,
20
+ * `glob.match(refPattern, ref)`, `Date.now() < expiresAt`).
21
+ */
22
+ export const UserPrincipal = type({
23
+ kind: "'user'",
24
+ principalId: "string",
25
+ tenantId: "string",
26
+ authz: {
27
+ effect: "'allow' | 'deny'",
28
+ resource: "string",
29
+ grantVerb: "string",
30
+ },
31
+ tokenClaims: {
32
+ refPattern: "string",
33
+ actions: RepoActionSchema.array(),
34
+ expiresAt: "number",
35
+ },
36
+ });
37
+ /**
38
+ * Regex defining the shape of a valid `RepoId.id`. The substrate
39
+ * validates against this at every public operation and throws an Error
40
+ * prefixed with `"repo_id_invalid: "` on mismatch.
41
+ */
42
+ export const SAFE_REPO_ID = /^[a-zA-Z0-9_-]+$/;