@agentshouse/mdruntime-adapter 0.1.0-cedb8e57837a7 → 0.1.0-cf059d0c8e9c0

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 (67) hide show
  1. package/README.md +2 -2
  2. package/dist/actions.d.ts +3 -68
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +1 -355
  5. package/dist/adapter.d.ts.map +1 -1
  6. package/dist/adapter.js +196 -410
  7. package/dist/app-reference.d.ts +2 -0
  8. package/dist/app-reference.d.ts.map +1 -0
  9. package/dist/app-reference.js +2 -0
  10. package/dist/crockford.d.ts +4 -0
  11. package/dist/crockford.d.ts.map +1 -0
  12. package/dist/crockford.js +3 -0
  13. package/dist/discovery.d.ts +12 -12
  14. package/dist/discovery.d.ts.map +1 -1
  15. package/dist/discovery.js +44 -51
  16. package/dist/history.d.ts +12 -0
  17. package/dist/history.d.ts.map +1 -0
  18. package/dist/history.js +91 -0
  19. package/dist/house-model.d.ts +2249 -0
  20. package/dist/house-model.d.ts.map +1 -0
  21. package/dist/house-model.js +1237 -0
  22. package/dist/index.d.ts +10 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -1
  25. package/dist/model.d.ts +5 -2
  26. package/dist/model.d.ts.map +1 -1
  27. package/dist/model.js +10 -29
  28. package/dist/part.d.ts +14 -0
  29. package/dist/part.d.ts.map +1 -0
  30. package/dist/part.js +90 -0
  31. package/dist/read.d.ts +27 -59
  32. package/dist/read.d.ts.map +1 -1
  33. package/dist/read.js +167 -266
  34. package/dist/refusal.d.ts +0 -3
  35. package/dist/refusal.d.ts.map +1 -1
  36. package/dist/refusal.js +0 -5
  37. package/dist/room-stream.d.ts +19 -0
  38. package/dist/room-stream.d.ts.map +1 -0
  39. package/dist/room-stream.js +57 -0
  40. package/dist/room-types.d.ts +7 -0
  41. package/dist/room-types.d.ts.map +1 -0
  42. package/dist/room-types.js +126 -0
  43. package/dist/site.d.ts.map +1 -1
  44. package/dist/site.js +81 -193
  45. package/dist/stream.d.ts +13 -13
  46. package/dist/stream.d.ts.map +1 -1
  47. package/dist/stream.js +161 -409
  48. package/dist/transport.d.ts +49 -39
  49. package/dist/transport.d.ts.map +1 -1
  50. package/dist/transport.js +53 -11
  51. package/dist/upload.d.ts +12 -0
  52. package/dist/upload.d.ts.map +1 -0
  53. package/dist/upload.js +63 -0
  54. package/dist/write.d.ts +4 -0
  55. package/dist/write.d.ts.map +1 -1
  56. package/dist/write.js +22 -18
  57. package/package.json +8 -4
  58. package/AGENTS.md +0 -304
  59. package/dist/gather.d.ts +0 -4
  60. package/dist/gather.d.ts.map +0 -1
  61. package/dist/gather.js +0 -35
  62. package/dist/handed.d.ts +0 -15
  63. package/dist/handed.d.ts.map +0 -1
  64. package/dist/handed.js +0 -45
  65. package/dist/referrer.d.ts +0 -7
  66. package/dist/referrer.d.ts.map +0 -1
  67. package/dist/referrer.js +0 -35
package/dist/adapter.js CHANGED
@@ -1,67 +1,47 @@
1
- import { sortedKeys, stepped } from '@agentshouse/mdruntime';
2
- import { DISCOVER_PREFIXES, capabilities, discoveredRoom, mapDiscovery, parseDiscovery, trackedFrom, } from './discovery.js';
3
- import { partModels, streamedModels, viewModels } from './model.js';
4
- import { gathered, gatheredEach, selections } from './gather.js';
1
+ import { stepped } from '@agentshouse/mdruntime';
2
+ import { capabilities, discoveredRoom, mapDiscovery, parseAuthorityChange, parseDiscovery, trackedFrom, } from './discovery.js';
5
3
  import { createSiteAdapter } from './site.js';
6
- import { authorityRoot, bootstrapping, matchesScope, parseEnumeration, parseEnvelope, parseGrant, refreshFrom, viewOf, wholeView, } from './read.js';
4
+ import { authorityRoot, bootstrapping, parseFileAddress, parseGrant, } from './read.js';
5
+ import { roomStreams } from './room-stream.js';
7
6
  import { sha256Hex } from './digest.js';
8
- import { acceptedHanded, enumeratedHanded, heldHanded, scopeHanded, streamedHanded, } from './handed.js';
7
+ import { finishedUpload, houseUpload } from './upload.js';
9
8
  import { houseAction } from './actions.js';
10
- import { classifyDoor, classifyRead, discoveryFailure, doorRefusal } from './refusal.js';
11
- import { houseNotifications } from './stream.js';
12
- import { DOOR_BATCH, DOOR_BATCH_UPLOAD, DOOR_BOOTSTRAP, DOOR_DISCOVER, DOOR_ENUMERATE, DOOR_FILES_GET, DOOR_REVISIONS, } from './transport.js';
9
+ import { classifyDoor, doorRefusal } from './refusal.js';
10
+ import { houseNotifications, } from './stream.js';
11
+ import { houseDoorRoute, } from './transport.js';
13
12
  import { batchPaths, deltaBody, deltaBytes, deltaDigest, historyOutcome, houseChanges, houseMove, mutationOutcome, operationHead, rejectedMapping, revisionBody, uploadable, } from './write.js';
14
- import { houseReferrer } from './referrer.js';
15
- import { followsVersion, logPosition, presented, reachedVersion, roomVersion, } from './version.js';
13
+ import { HOUSE_NAMEABLE_ROOM_TYPES, HOUSE_ROOM_TYPES } from './room-types.js';
14
+ import { followsVersion, logPosition, presented, reachedVersion, roomVersion } from './version.js';
16
15
  const UNCERTAIN = { kind: 'transport-uncertainty' };
17
16
  const UNCERTAIN_READ = { kind: 'transport-uncertainty' };
17
+ const OCTET_STREAM = 'application/octet-stream';
18
+ const EXPIRED_ADDRESS = 403;
18
19
  function lapsedGrant(outcome) {
19
20
  return outcome.kind === 'rejected' && outcome.code === 'upload_unavailable';
20
21
  }
21
- const heldPaths = new WeakMap();
22
- async function sortedHeld(held) {
23
- const known = heldPaths.get(held.documents);
24
- if (known !== undefined)
25
- return known;
26
- const sorted = await stepped(sortedKeys(held.documents));
27
- heldPaths.set(held.documents, sorted);
28
- return sorted;
29
- }
30
- function heldFiles(authority, held, paths, after, bound) {
31
- let low = 0;
32
- for (let high = paths.length; low < high;) {
33
- const middle = (low + high) >>> 1;
34
- if (after !== null && paths[middle] <= after)
35
- low = middle + 1;
36
- else
37
- high = middle;
38
- }
39
- const files = [];
40
- let through = null;
41
- for (let at = low; at < paths.length && through === null; at += 1) {
42
- const revision = held.revisions[paths[at]];
43
- if (revision === undefined)
44
- continue;
45
- if (files.length === bound)
46
- through = files.at(-1).path;
47
- else
48
- files.push({ path: paths[at], revision });
49
- }
22
+ async function heldFiles(authority, held, bound) {
23
+ const listed = await held.files(null, bound + 1);
24
+ const files = listed.slice(0, bound);
25
+ const through = listed.length > bound ? (files.at(-1)?.path ?? null) : null;
50
26
  return files.length === 0 ? null : { room: authority, through, files };
51
27
  }
52
28
  async function reused(scope, held) {
53
29
  const sent = new Set(scope.contents.map((entry) => entry.path));
54
30
  const contents = [...scope.contents];
55
- for (const file of scope.files) {
56
- if (file.binary !== undefined || sent.has(file.path))
57
- continue;
58
- const content = held?.documents[file.path];
59
- if (content === undefined ||
60
- held?.revisions[file.path] !== file.revision ||
61
- (await sha256Hex(content)) !== file.sha256) {
31
+ const asked = scope.files.filter((file) => file.binary === undefined && !sent.has(file.path));
32
+ if (asked.length === 0)
33
+ return scope;
34
+ if (held === undefined)
35
+ return null;
36
+ const kept = await held.contents(asked.map(({ path }) => path));
37
+ for (const file of asked) {
38
+ const found = kept.get(file.path);
39
+ if (found === undefined ||
40
+ found.revision !== file.revision ||
41
+ (await sha256Hex(found.content)) !== file.sha256) {
62
42
  return null;
63
43
  }
64
- contents.push({ path: file.path, content });
44
+ contents.push({ path: file.path, content: found.content });
65
45
  }
66
46
  return { ...scope, contents };
67
47
  }
@@ -69,136 +49,84 @@ export function createAgentsHouseAdapter(options) {
69
49
  if ('site' in options)
70
50
  return createSiteAdapter(options.site, options.snapshot === true);
71
51
  const transport = options.transport;
72
- const notifications = houseNotifications(transport, 'credential');
52
+ const notifications = houseNotifications(transport, 'credential', {
53
+ opened: async (frame) => (frame === null ? null : opened(frame)),
54
+ changed,
55
+ });
73
56
  const tracked = new Map();
74
- const handed = new Map();
57
+ const rooms = roomStreams('credential');
58
+ const unreused = new Set();
75
59
  let bounds;
76
60
  function declared() {
77
61
  if (bounds === undefined)
78
62
  throw new Error('no Discovery declared the bounds');
79
63
  return bounds;
80
64
  }
81
- function hand(locator, steps) {
82
- const held = handed.get(locator);
83
- const next = stepped(steps);
84
- if (!(next instanceof Promise)) {
85
- handed.set(locator, next);
86
- return;
87
- }
88
- return next.then((room) => {
89
- if (handed.get(locator) === held)
90
- handed.set(locator, room);
91
- else
92
- handed.delete(locator);
93
- });
65
+ function opened(frame) {
66
+ const document = parseDiscovery(frame);
67
+ if (document === null)
68
+ return null;
69
+ bounds = document.bounds;
70
+ tracked.clear();
71
+ for (const entry of trackedFrom(document))
72
+ tracked.set(entry.locator, entry);
73
+ notifications.track([...tracked.values()]);
74
+ return {
75
+ ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document.actions)),
76
+ member: document.member,
77
+ };
94
78
  }
95
- async function bootstrappedRoom(envelope, sent, asked, continued) {
96
- const { entry } = asked;
97
- if (sent === undefined)
98
- return { kind: 'room-not-readable', locator: entry.locator };
99
- const scope = await reused(sent, asked.held);
79
+ function changed(frame) {
80
+ const change = parseAuthorityChange(frame);
81
+ if (change === null)
82
+ return null;
83
+ for (const locator of change.left)
84
+ tracked.delete(locator);
85
+ for (const entry of trackedFrom(change))
86
+ tracked.set(entry.locator, entry);
87
+ notifications.track([...tracked.values()]);
88
+ return {
89
+ kind: 'authority-changed',
90
+ rooms: change.authorities.map((authority) => discoveredRoom(authority)),
91
+ left: change.left,
92
+ member: change.member,
93
+ };
94
+ }
95
+ async function streamedPage(entry, token, held) {
96
+ const part = await rooms.part(entry, token);
97
+ if ('kind' in part)
98
+ return part;
99
+ const scope = await reused(part.scope, held);
100
100
  if (scope === null) {
101
- return asked.held === undefined
102
- ? UNCERTAIN_READ
103
- : continued === null
104
- ? bootstrapTogether({ entry, held: undefined })
105
- : continuedBootstrap({ entry, held: undefined }, continued);
101
+ unreused.add(entry.locator);
102
+ return UNCERTAIN_READ;
106
103
  }
107
- const cursor = envelope.next?.room === entry.authority ? envelope.next : undefined;
108
- const next = cursor?.path ?? null;
109
- if (continued === null && next === null)
110
- await hand(entry.locator, scopeHanded(scope));
111
- else
112
- handed.delete(entry.locator);
113
- notifications.reach(entry.authority, scope);
114
- const seen = [
115
- ...(continued?.seen ?? []),
116
- ...entry.schemas.filter((schema) => scope.files.some((file) => file.path === schema)),
117
- ];
118
- const models = await partModels(scope, entry, new Set(seen), next === null);
119
- const continuation = next === null
120
- ? null
121
- : { after: next, seen, ...(cursor?.cut === undefined ? {} : { cut: cursor.cut }) };
122
104
  return {
123
105
  ...(await stepped(bootstrapping(scope))),
124
- ...(models === undefined ? {} : { models }),
125
- ...(continuation === null ? {} : { pageToken: JSON.stringify(continuation) }),
106
+ ...(part.models === undefined ? {} : { models: part.models }),
107
+ ...(part.pageToken === undefined ? {} : { pageToken: part.pageToken }),
108
+ bytes: part.bytes,
126
109
  };
127
110
  }
128
- async function enumeratedPage(ask, page) {
129
- const answer = await transport.door(DOOR_ENUMERATE, {
130
- prefixes: [authorityRoot(ask.entry.handle)],
131
- positions: [
132
- {
133
- ...presented(ask.entry.authority, { position: page.after, logEpoch: ask.from.logEpoch }),
134
- page: { through: page.through, read: page.read },
135
- },
136
- ],
137
- });
138
- const failure = classifyRead(answer.status, answer.body, ask.entry.locator);
139
- if (failure !== null)
140
- return failure;
141
- const enumeration = parseEnumeration(answer.body, 'credential');
142
- if (enumeration === null)
143
- return UNCERTAIN_READ;
144
- return (viewOf(enumeration, ask.entry) ?? { kind: 'room-not-readable', locator: ask.entry.locator });
145
- }
146
- async function enumeratedRoom(enumeration, ask) {
147
- const first = viewOf(enumeration, ask.entry);
148
- if (first === undefined)
149
- return { kind: 'room-not-readable', locator: ask.entry.locator };
150
- const view = await wholeView(first, (page) => enumeratedPage(ask, page));
151
- if ('kind' in view)
152
- return view;
153
- notifications.reach(ask.entry.authority, view);
154
- const answer = await refreshFrom(view, ask.fromRoomVersion);
155
- if (answer.kind !== 'net-advancement')
156
- return answer;
157
- await hand(ask.entry.locator, enumeratedHanded(view, handed.get(ask.entry.locator)));
158
- const models = await viewModels(view, ask.entry);
159
- return models === undefined ? answer : { ...answer, models };
160
- }
161
- async function streamed(entry, request, from) {
162
- const held = handed.get(entry.locator);
163
- if (held?.roomVersion !== request.fromRoomVersion)
164
- return null;
165
- const advanced = await stepped(notifications.advance(entry, from, (path, revision) => held.revisions[path] === revision));
166
- if (advanced === null)
167
- return null;
168
- await hand(entry.locator, streamedHanded(held, advanced));
169
- const models = await streamedModels(advanced.schemas, entry, 'credential');
170
- return models === undefined ? advanced.page : { ...advanced.page, models };
111
+ async function openedRoom(entry, request) {
112
+ const reusing = unreused.delete(entry.locator) ? undefined : request.held;
113
+ const files = reusing === undefined
114
+ ? null
115
+ : await heldFiles(entry.authority, reusing, declared().max_held_files);
116
+ const answered = await transport.streamed(houseDoorRoute('bootstrap'), {
117
+ prefixes: [authorityRoot(entry.handle)],
118
+ ...(files === null ? {} : { held: [files] }),
119
+ }, request.signal ?? new AbortController().signal);
120
+ if (answered.room === null)
121
+ return classifyDoor(answered.status, answered.body, entry.locator) ?? UNCERTAIN_READ;
122
+ return rooms.head(entry, answered.room, request.arriving, request.signal);
171
123
  }
172
124
  function held(rooms) {
173
- const kept = rooms.flatMap((room) => {
125
+ notifications.send(rooms.flatMap((room) => {
174
126
  const entry = tracked.get(room.locator);
175
127
  const at = logPosition(room.roomVersion);
176
- if (entry === undefined || at === null)
177
- return [];
178
- handed.set(room.locator, heldHanded(room));
179
- notifications.reach(entry.authority, at);
180
- return [presented(entry.authority, at)];
181
- });
182
- notifications.send(kept);
183
- }
184
- async function adoptedMove(entry, into, outcome) {
185
- if (outcome.kind === 'accepted' && outcome.destination !== undefined) {
186
- await adopted(into, { kind: 'accepted', ...outcome.destination });
187
- }
188
- return adopted(entry, outcome);
189
- }
190
- async function adopted(entry, outcome) {
191
- if (outcome.kind !== 'accepted')
192
- return outcome;
193
- const held = handed.get(entry.locator);
194
- if (held === undefined)
195
- return outcome;
196
- const follows = followsVersion(held.roomVersion, outcome.roomVersion);
197
- await hand(entry.locator, acceptedHanded(held, outcome, follows ? outcome.roomVersion : held.roomVersion));
198
- const at = logPosition(outcome.roomVersion);
199
- if (follows && at !== null)
200
- notifications.reach(entry.authority, at);
201
- return outcome;
128
+ return entry === undefined || at === null ? [] : [presented(entry.authority, at)];
129
+ }));
202
130
  }
203
131
  function authorityFor(locator) {
204
132
  return tracked.get(locator);
@@ -217,136 +145,25 @@ export function createAgentsHouseAdapter(options) {
217
145
  return result;
218
146
  return { ...result, at: { locator, roomVersion: roomVersion({ position, logEpoch }) } };
219
147
  }
220
- async function holding(asks, after) {
221
- const bound = Math.floor(declared().max_held_files / Math.max(1, asks.filter((ask) => ask.held !== undefined).length));
222
- const held = [];
223
- for (const ask of asks) {
224
- if (ask.held === undefined || bound === 0)
225
- continue;
226
- const paths = await sortedHeld(ask.held);
227
- const files = heldFiles(ask.entry.authority, ask.held, paths, after, bound);
228
- if (files !== null)
229
- held.push(files);
230
- }
231
- return held;
232
- }
233
- async function bootstrapped(asks, answer) {
234
- let open = asks.map((asked, ask) => ({ entry: asked.entry, asked, ask }));
235
- while (open.length > 0) {
236
- const unreached = [];
237
- for (const part of selections(open, declared().max_prefixes)) {
238
- const held = await holding(part.map(({ asked }) => asked), null);
239
- const answered = await transport.door(DOOR_BOOTSTRAP, {
240
- prefixes: part.map(({ entry }) => authorityRoot(entry.handle)),
241
- ...(held.length === 0 ? {} : { held }),
242
- });
243
- const classified = part.map(({ entry }) => classifyDoor(answered.status, answered.body, entry.locator));
244
- const envelope = classified[0] === null ? await stepped(parseEnvelope(answered.body)) : null;
245
- for (const [index, asked] of part.entries()) {
246
- const failure = classified[index];
247
- if (failure !== null && failure !== undefined) {
248
- answer(asked.ask, failure);
249
- continue;
250
- }
251
- if (envelope === null) {
252
- answer(asked.ask, UNCERTAIN_READ);
253
- continue;
254
- }
255
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, asked.entry));
256
- if (scope === undefined && envelope.next !== undefined) {
257
- unreached.push(asked);
258
- continue;
259
- }
260
- answer(asked.ask, await bootstrappedRoom(envelope, scope, asked.asked, null));
261
- }
262
- }
263
- open = unreached;
264
- }
265
- }
266
- async function continuedBootstrap(asked, continued) {
267
- const { entry } = asked;
268
- const after = {
269
- room: entry.authority,
270
- path: continued.after,
271
- ...(continued.cut === undefined ? {} : { cut: continued.cut }),
272
- };
273
- const held = await holding([asked], continued.after);
274
- const answered = await transport.door(DOOR_BOOTSTRAP, {
275
- prefixes: [authorityRoot(entry.handle)],
276
- after,
277
- ...(held.length === 0 ? {} : { held }),
278
- });
279
- const failure = classifyRead(answered.status, answered.body, entry.locator);
280
- if (failure !== null)
281
- return failure;
282
- const envelope = await stepped(parseEnvelope(answered.body));
283
- if (envelope === null)
284
- return UNCERTAIN_READ;
285
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, entry));
286
- return bootstrappedRoom(envelope, scope, asked, continued);
287
- }
288
- function layered(asks) {
289
- const depths = new Map();
290
- const layers = [];
291
- for (const ask of asks) {
292
- const depth = depths.get(ask.ask.entry.authority) ?? 0;
293
- depths.set(ask.ask.entry.authority, depth + 1);
294
- (layers[depth] ??= []).push(ask);
295
- }
296
- return layers;
297
- }
298
- async function enumerated(asks) {
299
- const answers = [];
300
- const parts = layered(asks.map((ask, index) => ({ ask, index }))).flatMap((layer) => selections(layer, declared().max_prefixes));
301
- for (const part of parts) {
302
- let open = [...part];
303
- while (open.length > 0) {
304
- const answer = await transport.door(DOOR_ENUMERATE, {
305
- prefixes: open.map(({ ask }) => authorityRoot(ask.entry.handle)),
306
- positions: open.map(({ ask }) => presented(ask.entry.authority, ask.from)),
307
- });
308
- const classified = open.map(({ ask }) => classifyRead(answer.status, answer.body, ask.entry.locator));
309
- if (classified[0]?.kind === 'beyond-horizon') {
310
- const named = doorRefusal(answer.body)?.fields.authority;
311
- const expired = open.filter(({ ask }) => ask.entry.authority === named);
312
- const answered = expired.length === 0 ? UNCERTAIN_READ : { kind: 'beyond-horizon' };
313
- const gone = expired.length === 0 ? open : expired;
314
- for (const ask of gone)
315
- answers[ask.index] = answered;
316
- open = open.filter((ask) => !gone.includes(ask));
317
- continue;
318
- }
319
- const enumeration = classified[0] === null ? parseEnumeration(answer.body, 'credential') : null;
320
- for (const [answerIndex, { ask, index }] of open.entries()) {
321
- const failure = classified[answerIndex];
322
- if (failure !== null && failure !== undefined) {
323
- answers[index] = failure;
324
- continue;
325
- }
326
- answers[index] =
327
- enumeration === null ? UNCERTAIN_READ : await enumeratedRoom(enumeration, ask);
328
- }
329
- open = [];
330
- }
331
- }
332
- return answers;
333
- }
334
- const bootstrapTogether = gatheredEach(bootstrapped);
335
- const enumerateTogether = gathered(enumerated);
336
148
  async function uploaded(head, changes, delta, bytes, entry, handed, keep) {
337
149
  const paths = batchPaths(changes);
338
150
  const sha256 = await deltaDigest(delta);
339
151
  if (handed !== undefined && handed.sha256 === sha256) {
340
152
  return { body: { ...head, paths, upload: handed, runtime: notifications.runtime } };
341
153
  }
342
- const minted = await transport.door(DOOR_BATCH_UPLOAD, { ...head, paths, bytes, sha256 });
154
+ const minted = await transport.door(houseDoorRoute('batch-upload'), {
155
+ ...head,
156
+ paths,
157
+ bytes,
158
+ sha256,
159
+ });
343
160
  if (minted.status !== 200) {
344
161
  return {
345
162
  outcome: await stepped(mutationOutcome(minted.status, minted.body, entry.locator, entry.handle)),
346
163
  };
347
164
  }
348
165
  const grant = parseGrant(minted.body);
349
- if (grant === null || grant.method !== 'POST')
166
+ if (grant === null)
350
167
  return { outcome: UNCERTAIN };
351
168
  const delivered = await transport.transfer({
352
169
  act: 'authored_batch_upload',
@@ -356,48 +173,43 @@ export function createAgentsHouseAdapter(options) {
356
173
  if (delivered.status !== 200)
357
174
  return { outcome: UNCERTAIN };
358
175
  const holding = parseGrant(delivered.body);
359
- if (holding === null || holding.method !== 'POST')
176
+ if (holding === null)
360
177
  return { outcome: UNCERTAIN };
361
178
  const upload = { url: holding.url, operation: holding.operation, bytes, sha256 };
362
179
  await keep(upload);
363
180
  return { body: { ...head, paths, upload, runtime: notifications.runtime } };
364
181
  }
365
- async function staged(answer, batch) {
182
+ async function staged(answer, { changeBatch: batch, file }) {
366
183
  const refusal = doorRefusal(answer.body);
367
184
  if (refusal?.code !== 'files_unstaged')
368
185
  return null;
369
- const grants = refusal.fields.upload;
370
- if (!Array.isArray(grants))
186
+ const uploads = refusal.fields.upload;
187
+ if (!Array.isArray(uploads))
371
188
  return false;
372
- for (const grant of grants) {
373
- const { url, operation, sha256 } = grant;
374
- const change = batch.changes.find((held) => held.kind === 'binary' && held.sha256 === sha256);
375
- if (typeof url !== 'string' || typeof operation !== 'string' || change === undefined)
189
+ for (const named of uploads) {
190
+ const upload = houseUpload(named);
191
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.sha256 === named.sha256);
192
+ if (upload === null || change === undefined)
376
193
  return false;
377
- const delivered = await transport.transfer({
378
- act: 'binary_stage',
379
- url,
380
- body: change.blob,
381
- operation,
382
- });
383
- if (delivered.status !== 200)
194
+ const finished = await finishedUpload(transport, upload, await file(change.localId));
195
+ if (finished?.body?.staged !== true)
384
196
  return false;
385
197
  }
386
198
  return true;
387
199
  }
388
- async function submit(batch, head, changes, delta, bytes, entry, handed, keep, into) {
200
+ async function submit(request, head, changes, delta, bytes, entry, handed, keep, into) {
389
201
  const inline = { ...head, changes, runtime: notifications.runtime };
390
202
  const prepared = uploadable(inline, bytes, declared())
391
203
  ? await uploaded(head, changes, delta, bytes, entry, handed, keep)
392
204
  : { body: inline };
393
205
  if ('outcome' in prepared)
394
206
  return prepared.outcome;
395
- const answer = await transport.door(DOOR_BATCH, prepared.body);
396
- const stagedFiles = await staged(answer, batch);
207
+ const answer = await transport.door(houseDoorRoute('batch'), prepared.body);
208
+ const stagedFiles = await staged(answer, request);
397
209
  if (stagedFiles === false)
398
210
  return UNCERTAIN;
399
- const settled = stagedFiles === null ? answer : await transport.door(DOOR_BATCH, prepared.body);
400
- if (stagedFiles === true && (await staged(settled, batch)) !== null)
211
+ const settled = stagedFiles === null ? answer : await transport.door(houseDoorRoute('batch'), prepared.body);
212
+ if (stagedFiles === true && (await staged(settled, request)) !== null)
401
213
  return UNCERTAIN;
402
214
  return stepped(mutationOutcome(settled.status, settled.body, entry.locator, entry.handle, into === undefined
403
215
  ? undefined
@@ -406,7 +218,7 @@ export function createAgentsHouseAdapter(options) {
406
218
  destination: { authority: into.authority, handle: into.handle },
407
219
  }));
408
220
  }
409
- function granted(answer, locator) {
221
+ function addressed(answer, locator) {
410
222
  const failure = classifyDoor(answer.status, answer.body, locator);
411
223
  if (failure !== null) {
412
224
  return failure.kind === 'scoped-refusal'
@@ -418,99 +230,71 @@ export function createAgentsHouseAdapter(options) {
418
230
  }
419
231
  : failure;
420
232
  }
421
- const grant = parseGrant(answer.body);
422
- const name = answer.body.name;
423
- if (grant === null || grant.method !== 'GET' || typeof name !== 'string') {
424
- return UNCERTAIN_READ;
425
- }
426
- return { kind: 'link', url: grant.url, name };
233
+ const address = parseFileAddress(answer.body);
234
+ return address === null ? UNCERTAIN_READ : { kind: 'address', ...address };
235
+ }
236
+ function linked(answer, locator) {
237
+ const address = addressed(answer, locator);
238
+ return address.kind === 'address'
239
+ ? { kind: 'link', url: address.url, name: address.name, mediaType: address.mediaType }
240
+ : address;
427
241
  }
428
- async function downloaded(answer, locator) {
429
- const link = granted(answer, locator);
430
- if (link.kind !== 'link')
431
- return link;
432
- const read = await transport.download(link.url);
433
- if (read.status !== 200 || read.blob === null)
242
+ function refusedAttach(answer, locator) {
243
+ const failure = classifyDoor(answer.status, answer.body, locator);
244
+ if (failure === null)
245
+ return null;
246
+ return failure.kind === 'scoped-refusal'
247
+ ? {
248
+ kind: 'rejected',
249
+ code: failure.code,
250
+ fields: failure.fields,
251
+ retryable: failure.retryable,
252
+ }
253
+ : failure;
254
+ }
255
+ async function downloaded(ask, locator) {
256
+ let address = addressed(await ask(), locator);
257
+ if (address.kind !== 'address')
258
+ return address;
259
+ let read = await transport.download(address.url);
260
+ if (read.status === EXPIRED_ADDRESS) {
261
+ address = addressed(await ask(), locator);
262
+ if (address.kind !== 'address')
263
+ return address;
264
+ read = await transport.download(address.url);
265
+ }
266
+ if (read.status !== 200 || read.body === null)
434
267
  return UNCERTAIN_READ;
435
- return { kind: 'binary', blob: read.blob };
268
+ return {
269
+ kind: 'binary',
270
+ body: read.body,
271
+ mediaType: address.mediaType,
272
+ sha256: address.sha256,
273
+ };
436
274
  }
437
- function pathGrant(request) {
275
+ function pathAddress(request) {
438
276
  const entry = authorityFor(request.locator);
439
277
  if (entry === undefined)
440
278
  return null;
441
- return transport.door(DOOR_FILES_GET, {
279
+ return () => transport.door(houseDoorRoute('files/get'), {
442
280
  room_ref: entry.authority,
443
281
  path: request.path,
444
282
  revision: request.revision,
445
283
  });
446
284
  }
447
285
  return {
286
+ roomTypes: HOUSE_ROOM_TYPES,
287
+ nameableRoomTypes: HOUSE_NAMEABLE_ROOM_TYPES,
448
288
  async authorize() {
449
289
  return options.authorize();
450
290
  },
451
- async discover(_authorization, scope) {
452
- const answer = await transport.door(DOOR_DISCOVER, {
453
- prefixes: [...DISCOVER_PREFIXES],
454
- ...(scope === undefined ? {} : { rooms: scope.authorities }),
455
- });
456
- const failed = discoveryFailure(answer.status, answer.body);
457
- if (failed !== null)
458
- return failed;
459
- const document = parseDiscovery(answer.body);
460
- if (document === null)
461
- return { kind: 'transport-uncertainty' };
462
- bounds = document.bounds;
463
- const entries = trackedFrom(document);
464
- const covered = scope === undefined
465
- ? null
466
- : [
467
- ...new Set([
468
- ...[...tracked.values()]
469
- .filter((entry) => scope.authorities.includes(entry.authority))
470
- .map((entry) => entry.locator),
471
- ...entries.map((entry) => entry.locator),
472
- ]),
473
- ];
474
- if (covered === null)
475
- tracked.clear();
476
- else
477
- for (const locator of covered)
478
- tracked.delete(locator);
479
- for (const entry of entries)
480
- tracked.set(entry.locator, entry);
481
- for (const locator of handed.keys())
482
- if (!tracked.has(locator))
483
- handed.delete(locator);
484
- notifications.track([...tracked.values()]);
485
- return {
486
- ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document.actions, [...tracked.values()])),
487
- member: document.member,
488
- ...(covered === null ? {} : { covered }),
489
- };
490
- },
491
291
  async bootstrap(_authorization, request) {
492
292
  const entry = authorityFor(request.locator);
493
293
  if (entry === undefined)
494
294
  return { kind: 'room-not-readable', locator: request.locator };
495
- const asked = { entry, held: request.held };
496
- if (request.pageToken !== undefined) {
497
- return continuedBootstrap(asked, JSON.parse(request.pageToken));
498
- }
499
- return bootstrapTogether(asked);
500
- },
501
- async refresh(_authorization, request) {
502
- const entry = authorityFor(request.locator);
503
- if (entry === undefined)
504
- return { kind: 'room-not-readable', locator: request.locator };
505
- if (request.pageToken !== undefined)
506
- return { kind: 'transport-uncertainty' };
507
- const from = logPosition(request.fromRoomVersion);
508
- if (from === null)
509
- return { kind: 'beyond-horizon' };
510
- const answered = await streamed(entry, request, from);
511
- if (answered !== null)
512
- return answered;
513
- return enumerateTogether({ entry, from, fromRoomVersion: request.fromRoomVersion });
295
+ return request.pageToken === undefined
296
+ ? openedRoom(entry, request)
297
+ : streamedPage(entry, request.pageToken, request.held);
514
298
  },
515
299
  async mutate(_authorization, request, keep) {
516
300
  const entry = authorityFor(request.locator);
@@ -531,18 +315,17 @@ export function createAgentsHouseAdapter(options) {
531
315
  const delta = deltaBody(mapped.changes);
532
316
  const bytes = deltaBytes(delta);
533
317
  const handed = request.upload;
534
- const answer = (upload) => submit(batch, head, mapped.changes, delta, bytes, entry, upload, keep, into);
535
- const settle = (outcome) => into === undefined ? adopted(entry, outcome) : adoptedMove(entry, into, outcome);
318
+ const answer = (upload) => submit(request, head, mapped.changes, delta, bytes, entry, upload, keep, into);
536
319
  const outcome = await answer(handed);
537
320
  if (handed === undefined || !lapsedGrant(outcome))
538
- return settle(outcome);
539
- return settle(await answer(undefined));
321
+ return outcome;
322
+ return answer(undefined);
540
323
  },
541
324
  async history(_authorization, request) {
542
325
  const entry = authorityFor(request.locator);
543
326
  if (entry === undefined)
544
327
  return { kind: 'room-not-readable', locator: request.locator };
545
- const answer = await transport.door(DOOR_REVISIONS, revisionBody({
328
+ const answer = await transport.door(houseDoorRoute('revisions'), revisionBody({
546
329
  path: request.path,
547
330
  handle: entry.handle,
548
331
  ...(request.revision === undefined ? {} : { revision: request.revision }),
@@ -554,36 +337,45 @@ export function createAgentsHouseAdapter(options) {
554
337
  });
555
338
  },
556
339
  async binary(_authorization, request) {
557
- if (request.kind === 'path') {
558
- const asked = pathGrant(request);
559
- if (asked === null)
560
- return { kind: 'room-not-readable', locator: request.locator };
561
- return downloaded(await asked, request.locator);
562
- }
563
- const referrer = houseReferrer(request.referrer, tracked);
564
- if (referrer === null) {
565
- return { kind: 'rejected', code: 'referrer_unknown', fields: {}, retryable: false };
566
- }
567
- const answer = await transport.door(DOOR_FILES_GET, {
568
- file: request.fileRef,
569
- referrer: referrer.referrer,
570
- });
571
- return downloaded(answer, referrer.locator);
340
+ const ask = pathAddress(request);
341
+ if (ask === null)
342
+ return { kind: 'room-not-readable', locator: request.locator };
343
+ return downloaded(ask, request.locator);
572
344
  },
573
345
  async link(_authorization, request) {
574
- const asked = pathGrant(request);
575
- if (asked === null)
346
+ const ask = pathAddress(request);
347
+ if (ask === null)
576
348
  return { kind: 'room-not-readable', locator: request.locator };
577
- return granted(await asked, request.locator);
349
+ return linked(await ask(), request.locator);
578
350
  },
579
- referrerScope(referrer) {
580
- const held = houseReferrer(referrer, tracked);
581
- if (held === null)
582
- return null;
583
- if (held.locator === '')
584
- return { kind: 'conversations' };
585
- const { path } = held.referrer;
586
- return { kind: 'room', locator: held.locator, ...(path === undefined ? {} : { path }) };
351
+ async attach(_authorization, request) {
352
+ const entry = authorityFor(request.locator);
353
+ if (entry === undefined)
354
+ return { kind: 'room-not-readable', locator: request.locator };
355
+ const declared = await transport.door(houseDoorRoute('files/attach'), {
356
+ room_ref: entry.authority,
357
+ name: request.name,
358
+ bytes: request.byteLength,
359
+ sha256: request.sha256,
360
+ media_type: request.mediaType.length > 0 ? request.mediaType : OCTET_STREAM,
361
+ });
362
+ const refused = refusedAttach(declared, request.locator);
363
+ if (refused !== null)
364
+ return refused;
365
+ const upload = houseUpload(declared.body?.upload);
366
+ if (upload === null)
367
+ return UNCERTAIN_READ;
368
+ const finished = await finishedUpload(transport, upload, request.blob);
369
+ if (finished === null)
370
+ return UNCERTAIN_READ;
371
+ const unfinished = refusedAttach(finished, request.locator);
372
+ if (unfinished !== null)
373
+ return unfinished;
374
+ const { path, revision } = (finished.body ?? {});
375
+ const root = `/rooms/${entry.handle}/`;
376
+ return typeof path === 'string' && path.startsWith(root) && typeof revision === 'string'
377
+ ? { kind: 'attached', path: path.slice(root.length), revision }
378
+ : UNCERTAIN_READ;
587
379
  },
588
380
  reached(locator, version, target) {
589
381
  return reachedVersion(version, target, authorityFor(locator)?.logEpoch);
@@ -596,13 +388,7 @@ export function createAgentsHouseAdapter(options) {
596
388
  },
597
389
  roomStream: {
598
390
  hold: held,
599
- reopen: notifications.reopen,
600
391
  observe: notifications.observe,
601
- published(locator, roomVersion) {
602
- const at = logPosition(roomVersion);
603
- if (at !== null)
604
- notifications.published(locator, at);
605
- },
606
392
  },
607
393
  conversations: notifications.conversations,
608
394
  serviceMode: notifications.serviceMode,