agentic-drive 0.11.1 → 0.13.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0
4
+
5
+ - Add the trusted host `claimResourceRef` helper. Commit app ownership and a reference atomically. Release on direct trash or removal and reclaim on restore. Report ownership conflicts as `REF_CONFLICT`. Require identity 0.12.0.
6
+
7
+ ## 0.12.0
8
+
9
+ - Add typed embedded navigation with `DriveLocation`, `location`, and `onNavigate`.
10
+ Mounted views expose `setLocation` and `getLocation` for host routing.
11
+ File links download in place. Explicit download locations return with a
12
+ history replace hint, and navigation cancels pending downloads.
13
+ - Keep the current location on option updates. Route embedded activity actions
14
+ through the host navigation API.
15
+
3
16
  ## 0.11.1
4
17
 
5
18
  - Allow `agentic-design` and `agentic-identity` releases below 1.0.0 while keeping the existing lower bounds. Test with identity 0.12.0 and design 0.10.1.
package/README.md CHANGED
@@ -134,6 +134,39 @@ try {
134
134
  - A browser page needs the Worker on its own origin for now: the Worker sends no CORS headers.
135
135
  - Share with a subject: `share("/pub", { kind: "human", id: "u2" }, "viewer")`.
136
136
 
137
+ ### Atomic app resource ownership
138
+
139
+ Trusted hosts can call `drive.claimResourceRef(ctx, path, ref, ownership, opts)`.
140
+ Use the same Store for Drive and identity sharing. Check app access first. This
141
+ host-only helper is not exposed by HTTP, CLI, or MCP.
142
+
143
+ ```ts
144
+ const item = await drive.claimResourceRef(ctx, "/Plan", "docs:plan", {
145
+ resourceId: "plan", type: "doc", subject: { kind: "human", id: "ana" },
146
+ });
147
+ await drive.trash(ctx, item.id);
148
+ await drive.restore(ctx, item.id);
149
+ ```
150
+
151
+ The same claim at the same path returns the existing reference, including when
152
+ calls run at the same time. Options accept `parents` to create missing folders.
153
+ Resource ids use 1 to 128 letters, digits, underscores, or hyphens. Resource types
154
+ start with a lowercase letter and use lowercase letters, digits, underscores, or
155
+ hyphens. The reference, resource, and owner grant commit in one transaction. A conflict
156
+ throws `DriveError` with code `REF_CONFLICT` (HTTP status 409). A failed path or
157
+ owner grant leaves no reference or claim. One subject can hold several references.
158
+ Direct trash releases one claim. Restore reacquires it and can conflict with a
159
+ new owner. Trashing a folder only hides its children; their claims stay until
160
+ those references are trashed or removed. Hard removal releases live claims.
161
+ If identity erase has removed an old claim, local trash and removal still succeed.
162
+ They keep any claim that another subject now holds. Call `erasePerson` as part of
163
+ account erase. It clears the subject from claimed references in other drives,
164
+ even when another person made the reference. Restore then restores the item
165
+ without recreating the erased subject's ownership.
166
+ Existing resources must already grant the subject ownership. The identity
167
+ lifecycle keeps existing shares and resources when it releases a claim.
168
+ This helper needs `agentic-identity` 0.12.0 or later.
169
+
137
170
  ### Other exports
138
171
 
139
172
  The root export, `agentic-drive`, also exports these names. The package typings give the type of each one.
@@ -189,7 +222,7 @@ checks), `upload`, `sniffMime`, `DEFAULT_MAX_BYTES` (upload helpers), `buildExpo
189
222
  (account plug-in). See Other exports and Features for behavior and limits.
190
223
 
191
224
  Root types: `ActivityAction`, `ActivityEntry`, `Action`, `Actor`, `ActorKind`,
192
- `Can`, `Ctx`, `Drive`, `DriveDeps`, `ErrorCode`, `Node`, `OnConflict`,
225
+ `Can`, `Ctx`, `Drive`, `DriveDeps`, `ErrorCode`, `Node`, `OnConflict`, `ResourceOwnership`,
193
226
  `UploadOptions`, `SearchQuery`, `BackfillOptions`, `BackfillResult`,
194
227
  `DriveAccount`, and `DriveAccountDeps`. They describe activity, callers, drive
195
228
  operations, uploads, migrations, and account operations.
@@ -248,7 +281,7 @@ view.unmount();
248
281
  ```
249
282
 
250
283
  `mount` returns a `MountedDrive` handle. `DriveBrowser` is the React component;
251
- `DriveUiOptions` is its configuration. `OnOpenItem` is the reference-open callback type, and `Item` is its argument type. The CSS path supplies its styles.
284
+ `DriveUiOptions` is its configuration. `DriveLocation` is the folder, trash, activity, or file location type. `DriveNavigationOptions` describes host history updates. `OnOpenItem` is the reference-open callback type, and `Item` is its argument type. The CSS path supplies its styles.
252
285
 
253
286
  ### CLI, MCP, and adapters
254
287
 
@@ -323,6 +356,62 @@ drive.update({ baseUrl: location.origin, getToken: () => session.token, onOpenIt
323
356
 
324
357
  `DriveUiOptions` exports this optional hook for both `mount` and `DriveBrowser`. `update()` can replace or remove it. React hosts can change the `onOpenItem` prop without resetting the current folder. The callback is synchronous.
325
358
 
359
+ ### Embed navigation
360
+
361
+ Use the exported `DriveLocation` type to select a folder, trash, item activity,
362
+ or file download. A folder location has `{ kind: "folder", path: "/" }`.
363
+ Its `path` accepts a folder id, an absolute path such as `"/Work/Plans"`,
364
+ or a path from a folder id. Other locations use `{ kind: "trash" }`,
365
+ `{ kind: "activity", itemId }`, or `{ kind: "file", itemId }`.
366
+ The server checks access for each request. An inaccessible folder shows an error.
367
+
368
+ `mount` starts at `options.location`, or root when it is absent.
369
+ `setLocation` changes the view without remounting the drive.
370
+ `getLocation` returns a copy of the active location. For a folder, pass its
371
+ `path` as `folder` to the client's `registerDocument` method.
372
+ Trash, activity, and downloads have no active folder.
373
+
374
+ ```ts
375
+ import { mount, type DriveLocation } from "agentic-drive/ui";
376
+
377
+ const readLocation = (): DriveLocation =>
378
+ JSON.parse(new URL(location.href).searchParams.get("drive") ??
379
+ '{"kind":"folder","path":"/"}'); // Validate URL data in your app.
380
+ const drive = mount(document.getElementById("drive")!, {
381
+ baseUrl: location.origin,
382
+ getToken: () => session.token,
383
+ location: readLocation(),
384
+ onNavigate(next, { replace }) {
385
+ const url = new URL(location.href);
386
+ url.searchParams.set("drive", JSON.stringify(next));
387
+ if (replace) history.replaceState(null, "", url);
388
+ else history.pushState(null, "", url);
389
+ },
390
+ });
391
+ addEventListener("popstate", () => drive.setLocation(readLocation()));
392
+ const active = drive.getLocation();
393
+ if (active.kind === "folder") {
394
+ await client.registerDocument({ name: "Plan", ref: "docs:plan", folder: active.path });
395
+ }
396
+ ```
397
+
398
+ `onNavigate` reports folder, breadcrumb, trash, and activity navigation with
399
+ `replace: false`. File links download in place without changing the location or
400
+ notifying the host. Errors leave the folder open. Navigation cancels pending
401
+ downloads, so a late response cannot move the view or start a download.
402
+ An explicit `file` location returns to its parent with `replace: true`. Use
403
+ `replaceState` for that return. Root returns use `"/"`.
404
+ Assigning a location through initial render, `setLocation`, or `update` does not
405
+ call the callback itself. An explicit file location starts a download; its completed return
406
+ does call the callback. An explicit file location that fails shows an error and
407
+ stays selected; the host can set a folder location to leave it.
408
+ `update` keeps the current location unless it receives a new `location`.
409
+ In React, `DriveBrowser location={current} onNavigate={setCurrent}` is controlled:
410
+ the host must apply the next location. Without `location`, React keeps it locally.
411
+ Changes to server or token options keep the location and reload its data.
412
+ The embed never changes the host URL itself.
413
+
414
+
326
415
  `getToken` returns 1 to 512 letters, digits, `-` or `_`, or `undefined` to send no `authorization` header. A bad `baseUrl` or a `getToken` that is not a function makes `mount` throw a `TypeError`. The server still checks every request against the token. The embed shows files and trash with upload, rename, move, share, and the "Created by agent" label. It has no sign-in form, because your app owns sign-in. It does not change the page address, and it has no theme switch. `agentic-design/styles.css` sets base styles for the whole page, and it follows the dark mode of the system unless your page sets `data-theme`. Add `agentic-design@">=0.8.0 <1.0.0"` to your own dependencies, because your page imports its files. The Worker sends no CORS headers, so the server must be on the origin of your page. Set `baseUrl` to the same address as `DRIVE_PUBLIC_URL`, or the UI refuses its download links. `unmount` and `update` abort the requests that run and stop an upload that has more files to send. The UI keeps the list or grid choice in `localStorage` under `agentic-drive-view`, and it has its own heading for each view. Dialogs, menus, and messages render in `document.body`, outside your element. A click with Ctrl, Cmd, Shift, or the middle button on a folder link does not open the folder. The class names start with `drive-`. The design-system classes start with `agd-`. `DriveBrowser` is the same UI as a React component. It closes its requests when it unmounts. Wrap it in `TooltipProvider` and `ToastProvider` from `agentic-design/react`. `react` and `react-dom` (version 19 or later) are optional peer dependencies. Install them in your project when you use `agentic-drive/ui`. The web UI that the Worker serves uses the same views.
327
416
 
328
417
  ### Serve the UI from the package
package/dist/account.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as Actor, D as Drive, C as Ctx } from './drive-CS1CLfqo.js';
1
+ import { A as Actor, D as Drive, C as Ctx } from './drive-BncyFxBL.js';
2
2
  import 'agentic-identity';
3
3
 
4
4
  interface DriveAccountDeps {
package/dist/account.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  driveAccount
3
- } from "./chunk-ECVUDAHL.js";
4
- import "./chunk-DB4F5EWD.js";
3
+ } from "./chunk-OHDNQ23Q.js";
4
+ import "./chunk-FTIWCBCI.js";
5
5
  export {
6
6
  driveAccount
7
7
  };
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.11.1" : readPackageVersion();
3
+ var VERSION = true ? "0.13.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
@@ -1,5 +1,5 @@
1
1
  // src/drive.ts
2
- import { EraseRefusedError, isMachineMade, StoreConflictError, MAX_FOLDER_DEPTH, PRINCIPAL_KINDS, ROLES, SUBJECT_KINDS, hasScope } from "agentic-identity";
2
+ import { ResourceClaimConflictError, EraseRefusedError, isMachineMade, StoreConflictError, MAX_FOLDER_DEPTH, PRINCIPAL_KINDS, ROLES, SUBJECT_KINDS, hasScope } from "agentic-identity";
3
3
  var ROOT_ID = "root";
4
4
  var NODES = "drive_nodes";
5
5
  var NAMES = "drive_names";
@@ -7,6 +7,7 @@ var ACTIVITY = "drive_activity";
7
7
  var ACTOR_KINDS = PRINCIPAL_KINDS;
8
8
  var DECIDED = /* @__PURE__ */ Symbol("decided");
9
9
  var ERROR_STATUS = Object.freeze({
10
+ REF_CONFLICT: 409,
10
11
  CROSS_DRIVE: 409,
11
12
  INVALID_SHARE: 400,
12
13
  TOO_MANY_ITEMS: 409,
@@ -354,7 +355,7 @@ function createDrive(deps) {
354
355
  if (node.kind !== "folder") throw new DriveError("NOT_A_FOLDER", `${node.name} is not a folder`);
355
356
  return node;
356
357
  }
357
- async function insert(ops, w, ctx, parent, rawName, fields, onConflict) {
358
+ async function insert(ops, w, ctx, parent, rawName, fields, onConflict, fixedId) {
358
359
  let { name, key } = parseName(rawName);
359
360
  const empty = w.fresh.has(parent.id);
360
361
  if (!empty) await need(ctx, "write", parent);
@@ -370,7 +371,7 @@ function createDrive(deps) {
370
371
  name = await freeName(ops, parent.owner, parent.id, name);
371
372
  key = parseName(name).key;
372
373
  }
373
- const id = newId();
374
+ const id = fixedId ?? newId();
374
375
  if (sharing) {
375
376
  const depth = await depthOf(ops, w, parent) + 1;
376
377
  if (depth > MAX_ITEM_DEPTH) throw new DriveError("TOO_DEEP", `an item can sit at most ${MAX_ITEM_DEPTH} folders deep`);
@@ -506,13 +507,31 @@ function createDrive(deps) {
506
507
  throw error;
507
508
  }
508
509
  }
509
- async function transactAs(ctx, fn) {
510
+ async function transactAs(ctx, fn, lifecycle) {
511
+ const run = async (mutate) => {
512
+ if (!lifecycle) return transact(mutate);
513
+ if (!sharing) throw new DriveError("INVALID_SHARE", "sharing is not set up");
514
+ const { ownership: o, id, release } = lifecycle;
515
+ let result;
516
+ try {
517
+ await sharing[release ? "releaseResourceClaim" : "claimResource"](o.resourceId, o.subject, id, async (tx, claims) => {
518
+ result = await mutate(tx);
519
+ if (!release) claims.ensureOwner(o.type);
520
+ });
521
+ } catch (error) {
522
+ if (release && error instanceof ResourceClaimConflictError) return transact(mutate);
523
+ if (error instanceof ResourceClaimConflictError) throw new DriveError("REF_CONFLICT", "the app resource has a conflicting owner or reference");
524
+ if (error instanceof StoreConflictError) throw new DriveError("BUSY", "the drive changed at the same time. Try again.");
525
+ throw error;
526
+ }
527
+ return result;
528
+ };
510
529
  const answers = /* @__PURE__ */ new Map();
511
530
  let runs = 0;
512
531
  let asked = false;
513
532
  for (; ; ) {
514
533
  try {
515
- return await transact((tx) => {
534
+ return await run((tx) => {
516
535
  if (++runs > 2 * MAX_ASKS) throw new DriveError("BUSY", "the drive changed at the same time. Try again.");
517
536
  if (runs > 1 && !asked) answers.clear();
518
537
  asked = false;
@@ -641,7 +660,17 @@ function createDrive(deps) {
641
660
  for (let i = 0; i < rows.length; i += size) {
642
661
  const chunk = rows.slice(i, i + size);
643
662
  for (const node of chunk) if (node.blobKey) await blobs.delete(node.blobKey);
644
- for (const node of chunk) await finishResource(principal, node.id);
663
+ for (const node of chunk) {
664
+ const current = await store.get(NODES, node.id);
665
+ if (current?.resourceOwnership && !current.trashed) await transactAs({ owner: node.owner, actor: node.updatedBy }, async (tx) => {
666
+ const held = await tx.get(NODES, node.id);
667
+ if (held) {
668
+ const { resourceOwnership: _released, resourceOwnerId: _id, resourceOwnerKind: _kind, ...rest } = held;
669
+ await tx.put(NODES, node.id, rest);
670
+ }
671
+ }, { ownership: current.resourceOwnership, id: node.id, release: true });
672
+ await finishResource(principal, node.id);
673
+ }
645
674
  for (const node of chunk) for (const entry of await allWhere(store, ACTIVITY, { itemId: node.id, owner: node.owner })) await store.delete(ACTIVITY, entry.id);
646
675
  await transact(async (tx) => {
647
676
  const held = [];
@@ -801,8 +830,9 @@ function createDrive(deps) {
801
830
  },
802
831
  /** Hides an item and, for a folder, everything under it. Nothing else changes. */
803
832
  async trash(ctx, loc) {
833
+ const probe = await resolve(store, ctx, loc);
804
834
  return transactAs(ctx, async (tx, ctx2) => {
805
- const node = await resolve(tx, ctx2, loc);
835
+ const node = await resolve(tx, ctx2, probe.id);
806
836
  if (node.id === ROOT_ID) throw new DriveError("IS_ROOT", "cannot trash the root");
807
837
  await need(ctx2, "write", node);
808
838
  const claim = await claimId(node.owner, node.parentId, node.nameKey);
@@ -814,7 +844,7 @@ function createDrive(deps) {
814
844
  await tx.put(ACTIVITY, entry.id, entry);
815
845
  if (held?.nodeId === node.id) await tx.delete(NAMES, claim);
816
846
  return trashed;
817
- });
847
+ }, probe.resourceOwnership ? { ownership: probe.resourceOwnership, id: probe.id, release: true } : void 0);
818
848
  },
819
849
  /** Items trashed on their own, newest first. */
820
850
  async listTrash(ctx) {
@@ -835,6 +865,7 @@ function createDrive(deps) {
835
865
  },
836
866
  /** Puts a trashed item back. It needs a live parent folder. */
837
867
  async restore(ctx, id, opts = {}) {
868
+ const ownership = (await fetch(store, ctx, id))?.resourceOwnership;
838
869
  let height = 0;
839
870
  if (sharing && opts.parentId !== void 0) {
840
871
  const probe = await fetch(store, ctx, id);
@@ -844,6 +875,10 @@ function createDrive(deps) {
844
875
  return transactAs(ctx, async (tx, ctx2) => {
845
876
  const node = await fetch(tx, ctx2, id);
846
877
  if (!node || node.id === ROOT_ID) throw new DriveError("NOT_FOUND", `no item ${id}`);
878
+ const current = node.resourceOwnership;
879
+ if (Boolean(current) !== Boolean(ownership) || current?.resourceId !== ownership?.resourceId || current?.type !== ownership?.type || current?.subject?.id !== ownership?.subject?.id || current?.subject?.kind !== ownership?.subject?.kind) {
880
+ throw new DriveError("BUSY", "the reference ownership changed at the same time. Try again.");
881
+ }
847
882
  if (!node.trashed) throw new DriveError("NOT_TRASHED", `${node.name} is not in the trash`);
848
883
  await need(ctx2, "write", node);
849
884
  const parent = opts.parentId === void 0 ? await rawNode(tx, node.parentId, node.owner, ctx2.actor) : await rawNode(tx, opts.parentId, ctx2.owner, ctx2.actor);
@@ -870,7 +905,7 @@ function createDrive(deps) {
870
905
  await tx.put(NAMES, await claimId(parent.owner, parent.id, key), { nodeId: restored.id, owner: parent.owner, parentId: parent.id });
871
906
  if (sharing && parent.id !== node.parentId) await tx.put("resources", restored.id, await resourceDoc(restored, parent));
872
907
  return restored;
873
- });
908
+ }, ownership ? { ownership, id } : void 0);
874
909
  },
875
910
  /** Live items that match every field given, sorted by path. */
876
911
  async search(ctx, q = {}) {
@@ -1021,6 +1056,48 @@ function createDrive(deps) {
1021
1056
  await store.put(ACTIVITY, ...logDoc(ctx, node, "share", { subjectKind: subject.kind, subjectId: subject.id, role: claim.role }));
1022
1057
  return node;
1023
1058
  },
1059
+ /** Trusted host only. Check app access before calling. Own the app resource and create its reference atomically. */
1060
+ async claimResourceRef(ctx, path, ref, ownership, opts = {}) {
1061
+ if (!REF.test(ref)) throw new DriveError("INVALID_REF", "a reference looks like docs:<id>");
1062
+ if (!ownership || typeof ownership !== "object" || typeof ownership.resourceId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(ownership.resourceId) || typeof ownership.type !== "string" || !/^[a-z][a-z0-9_-]*$/.test(ownership.type)) {
1063
+ throw new DriveError("INVALID_REF", "ownership needs a safe resource id and a lowercase resource type");
1064
+ }
1065
+ const subject = parseSubject(ownership.subject);
1066
+ const claim = { ...ownership, subject };
1067
+ for (let attempt = 0; ; attempt++) {
1068
+ let existing = null;
1069
+ try {
1070
+ existing = await resolve(store, ctx, path);
1071
+ } catch (error) {
1072
+ if (!(error instanceof DriveError) || error.code !== "NOT_FOUND") throw error;
1073
+ }
1074
+ const id = existing?.id ?? newId();
1075
+ if (typeof id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(id)) throw new DriveError("INVALID_REF", "the reference needs a safe id");
1076
+ try {
1077
+ return await transactAs(ctx, async (tx, ctx2) => {
1078
+ const w = batch();
1079
+ const { parent, name } = await parentAndName(tx, w, ctx2, path, opts.parents ?? false);
1080
+ const slot = await claimed(tx, parent.owner, parent.id, parseName(name).key);
1081
+ if (slot.node?.id === id) {
1082
+ await need(ctx2, "write", slot.node);
1083
+ const held = slot.node.resourceOwnership;
1084
+ if (slot.node.ref !== ref || !held || held.resourceId !== claim.resourceId || held.type !== claim.type || !held.subject || held.subject.id !== subject.id || held.subject.kind !== subject.kind) {
1085
+ throw new DriveError("REF_CONFLICT", "this path has a conflicting reference");
1086
+ }
1087
+ return slot.node;
1088
+ }
1089
+ if (existing) throw new DriveError("BUSY", "the reference moved at the same time. Try again.");
1090
+ const { node } = await insert(tx, w, ctx2, parent, name, { kind: "ref", ref }, "fail", id);
1091
+ const owned = { ...node, resourceOwnership: claim, resourceOwnerId: subject.id, resourceOwnerKind: subject.kind };
1092
+ w.put(NODES, id, owned);
1093
+ await w.apply(tx);
1094
+ return owned;
1095
+ }, { ownership: claim, id });
1096
+ } catch (error) {
1097
+ if (attempt >= 2 || !(error instanceof DriveError) || error.code !== "NAME_TAKEN") throw error;
1098
+ }
1099
+ }
1100
+ },
1024
1101
  /** An app deleted a document. Its references in this drive go to the trash. Returns how many. */
1025
1102
  async forgetDocument(ctx, ref) {
1026
1103
  if (!REF.test(ref)) throw new DriveError("INVALID_REF", `a reference looks like "docs:<id>", got ${JSON.stringify(ref)}`);
@@ -1121,6 +1198,18 @@ function createDrive(deps) {
1121
1198
  let invites = 0;
1122
1199
  for (const [actor, kind] of heldBy(subjectOf(ctx), ownedAgents)) grants += await sweep(await ofDrive(await allWhere(store, "grants", { subjectId: actor, subjectKind: kind })), "grants");
1123
1200
  for (const actor of actors) invites += await sweep(await ofDrive(await allWhere(store, "invites", { createdBy: actor })), "invites");
1201
+ for (const [actor, kind] of heldBy(subjectOf(ctx), ownedAgents)) {
1202
+ for (const { data } of await allWhere(store, NODES, { resourceOwnerId: actor, resourceOwnerKind: kind })) {
1203
+ const node = data;
1204
+ if (!node.resourceOwnership) continue;
1205
+ await transactAs(ctx, async (tx) => {
1206
+ const current = await tx.get(NODES, node.id);
1207
+ if (current?.resourceOwnerId !== actor || current.resourceOwnerKind !== kind) return;
1208
+ const { resourceOwnership: _claim, resourceOwnerId: _id, resourceOwnerKind: _kind, ...rest } = current;
1209
+ await tx.put(NODES, node.id, rest);
1210
+ }, { ownership: node.resourceOwnership, id: node.id, release: true });
1211
+ }
1212
+ }
1124
1213
  const todo = await madeBy(store, actors);
1125
1214
  for (let i = 0; i < todo.length; i += CHUNK) {
1126
1215
  await transact(async (tx) => {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DriveError,
3
3
  safeMime
4
- } from "./chunk-DB4F5EWD.js";
4
+ } from "./chunk-FTIWCBCI.js";
5
5
 
6
6
  // src/upload.ts
7
7
  var DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DriveError,
3
3
  publicNode
4
- } from "./chunk-DB4F5EWD.js";
4
+ } from "./chunk-FTIWCBCI.js";
5
5
 
6
6
  // src/account.ts
7
7
  var plain = (value) => JSON.parse(JSON.stringify(value));
@@ -11,6 +11,7 @@ var DriveApiError = class extends Error {
11
11
  };
12
12
  var BASE = "/api/v1/drive";
13
13
  var CODES = {
14
+ REF_CONFLICT: true,
14
15
  NOT_FOUND: true,
15
16
  NAME_TAKEN: true,
16
17
  BLOB_EXISTS: true,
package/dist/cli.js CHANGED
@@ -4,11 +4,11 @@ import {
4
4
  } from "./chunk-AB2RTTHJ.js";
5
5
  import {
6
6
  VERSION
7
- } from "./chunk-W27ZQYPT.js";
7
+ } from "./chunk-6TPC3BUW.js";
8
8
  import {
9
9
  buildExport,
10
10
  upload
11
- } from "./chunk-HJXRMOUY.js";
11
+ } from "./chunk-NPJG2MYP.js";
12
12
  import {
13
13
  AuthError,
14
14
  DriveError,
@@ -22,7 +22,7 @@ import {
22
22
  resolveToken,
23
23
  revokeKey,
24
24
  revokePrincipal
25
- } from "./chunk-DB4F5EWD.js";
25
+ } from "./chunk-FTIWCBCI.js";
26
26
 
27
27
  // src/cli.ts
28
28
  import { createReadStream, realpathSync, writeFileSync } from "fs";
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { i as ErrorCode, N as Node, d as ActivityEntry, S as SearchQuery } from './drive-CS1CLfqo.js';
2
- export { c as ActivityAction } from './drive-CS1CLfqo.js';
1
+ import { i as ErrorCode, N as Node, d as ActivityEntry, S as SearchQuery } from './drive-BncyFxBL.js';
2
+ export { c as ActivityAction } from './drive-BncyFxBL.js';
3
3
  import 'agentic-identity';
4
4
 
5
5
  type Role = "viewer" | "commenter" | "editor" | "owner";
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DriveApiError,
3
3
  createClient
4
- } from "./chunk-SEHT5Q5L.js";
4
+ } from "./chunk-TGOARFYZ.js";
5
5
  export {
6
6
  DriveApiError,
7
7
  createClient
@@ -1,5 +1,5 @@
1
1
  import * as agentic_identity from 'agentic-identity';
2
- import { PrincipalKind, Store, Blobs, PrincipalContext, createSharing, Subject, Role, SubjectKind } from 'agentic-identity';
2
+ import { PrincipalKind, Store, Blobs, PrincipalContext, Subject, SubjectKind, createSharing, Role } from 'agentic-identity';
3
3
 
4
4
  declare const ROOT_ID = "root";
5
5
  declare const ACTOR_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
@@ -38,7 +38,17 @@ type Action = "read" | "write" | "delete";
38
38
  type Sharing = ReturnType<typeof createSharing>;
39
39
  /** The seam for access. With `sharing`, the default is a role check through agentic-identity. */
40
40
  type Can = (ctx: Ctx, action: Action, node: Node) => boolean | Promise<boolean>;
41
+ type ResourceOwnership = {
42
+ resourceId: string;
43
+ subject: Subject;
44
+ type: string;
45
+ };
41
46
  type Node = {
47
+ /** Trusted host claim. Kept in trash so restore can reacquire it. */
48
+ resourceOwnership?: ResourceOwnership;
49
+ /** Indexed subject fields let account erase find claims in other drives. */
50
+ resourceOwnerId?: string;
51
+ resourceOwnerKind?: SubjectKind;
42
52
  id: string;
43
53
  owner: string;
44
54
  parentId: string;
@@ -63,7 +73,7 @@ type Node = {
63
73
  /** Set while a purge deletes the item. Nothing can see or change it then. */
64
74
  purging: boolean;
65
75
  };
66
- type ErrorCode = "NOT_FOUND" | "NAME_TAKEN" | "BLOB_EXISTS" | "INVALID_NAME" | "INVALID_PATH" | "INVALID_REF" | "NOT_A_FOLDER" | "NOT_A_FILE" | "MOVE_INTO_SELF" | "TOO_DEEP" | "PARENT_GONE" | "TRASHED" | "NOT_TRASHED" | "IS_ROOT" | "FORBIDDEN" | "BLOB_MISSING" | "TOO_LARGE" | "INVALID_EXPIRY" | "BUSY" | "CROSS_DRIVE" | "INVALID_SHARE" | "TOO_MANY_ITEMS";
76
+ type ErrorCode = "REF_CONFLICT" | "NOT_FOUND" | "NAME_TAKEN" | "BLOB_EXISTS" | "INVALID_NAME" | "INVALID_PATH" | "INVALID_REF" | "NOT_A_FOLDER" | "NOT_A_FILE" | "MOVE_INTO_SELF" | "TOO_DEEP" | "PARENT_GONE" | "TRASHED" | "NOT_TRASHED" | "IS_ROOT" | "FORBIDDEN" | "BLOB_MISSING" | "TOO_LARGE" | "INVALID_EXPIRY" | "BUSY" | "CROSS_DRIVE" | "INVALID_SHARE" | "TOO_MANY_ITEMS";
67
77
  /** The HTTP status of each error code. A host that serves the drive reuses it. */
68
78
  declare const ERROR_STATUS: Readonly<Record<ErrorCode, 400 | 403 | 404 | 409 | 413 | 500>>;
69
79
  declare class DriveError extends Error {
@@ -234,6 +244,10 @@ declare function createDrive(deps: DriveDeps): {
234
244
  parents?: boolean;
235
245
  onConflict?: OnConflict;
236
246
  }): Promise<Node>;
247
+ /** Trusted host only. Check app access before calling. Own the app resource and create its reference atomically. */
248
+ claimResourceRef(ctx: Ctx, path: string, ref: string, ownership: ResourceOwnership, opts?: {
249
+ parents?: boolean;
250
+ }): Promise<Node>;
237
251
  /** An app deleted a document. Its references in this drive go to the trash. Returns how many. */
238
252
  forgetDocument(ctx: Ctx, ref: string): Promise<{
239
253
  trashed: number;
@@ -248,7 +262,7 @@ declare function createDrive(deps: DriveDeps): {
248
262
  exportPerson(ctx: Ctx): Promise<{
249
263
  subject: {
250
264
  id: string;
251
- kind: "human" | "agent_of_human" | "agent_of_agent" | "agent" | "user_token" | null;
265
+ kind: "human" | "agent" | "agent_of_human" | "agent_of_agent" | "user_token" | null;
252
266
  };
253
267
  partial: boolean;
254
268
  authored: {
@@ -380,4 +394,4 @@ declare function createDrive(deps: DriveDeps): {
380
394
  }>;
381
395
  };
382
396
 
383
- export { type Actor as A, type Ctx as C, type Drive as D, ERROR_STATUS as E, type Node as N, type OnConflict as O, ROOT_ID as R, type SearchQuery as S, ACTOR_KINDS as a, type Action as b, type ActivityAction as c, type ActivityEntry as d, type ActorKind as e, type Can as f, type DriveDeps as g, DriveError as h, type ErrorCode as i, createDrive as j, parseSubject as k, parseName as p, subjectKindOf as s };
397
+ export { type Actor as A, type Ctx as C, type Drive as D, ERROR_STATUS as E, type Node as N, type OnConflict as O, type ResourceOwnership as R, type SearchQuery as S, ACTOR_KINDS as a, type Action as b, type ActivityAction as c, type ActivityEntry as d, type ActorKind as e, type Can as f, type DriveDeps as g, DriveError as h, type ErrorCode as i, ROOT_ID as j, createDrive as k, parseSubject as l, parseName as p, subjectKindOf as s };
package/dist/embed.d.ts CHANGED
@@ -1,12 +1,34 @@
1
1
  import * as react from 'react';
2
2
  import { Item } from './client.js';
3
- import './drive-CS1CLfqo.js';
3
+ import './drive-BncyFxBL.js';
4
4
  import 'agentic-identity';
5
5
 
6
6
  /** Return true to handle a reference open; false or void keeps the download action. */
7
7
  type OnOpenItem = (item: Item) => boolean | void;
8
8
 
9
+ /** Folder locators follow the SDK rules: "/" is root; use an id or an absolute path. */
10
+ type DriveLocation = {
11
+ kind: "folder";
12
+ path: string;
13
+ } | {
14
+ kind: "trash";
15
+ } | {
16
+ kind: "activity";
17
+ itemId: string;
18
+ } | {
19
+ kind: "file";
20
+ itemId: string;
21
+ };
22
+ /** Replace the current host history entry for a transient navigation. */
23
+ interface DriveNavigationOptions {
24
+ replace: boolean;
25
+ }
9
26
  interface DriveUiOptions {
27
+ /** Controlled in React. In mount/update, selects the location; omission preserves it. */
28
+ location?: DriveLocation;
29
+ /** Internal navigation. Controlled React hosts must apply the next location.
30
+ * Use replaceState when options.replace is true (an explicit download returns). */
31
+ onNavigate?: (location: DriveLocation, options: DriveNavigationOptions) => void;
10
32
  /** Runs when a person activates a reference item. Return true to skip the built-in download. */
11
33
  onOpenItem?: OnOpenItem;
12
34
  /** The address of the drive server, for example `https://drive.example.com`. Only its origin is used. */
@@ -15,15 +37,19 @@ interface DriveUiOptions {
15
37
  getToken: () => string | undefined | Promise<string | undefined>;
16
38
  }
17
39
  /** The folders and trash of the signed-in caller, with upload, share, and the machine-made label. It needs the tooltip and toast providers above it. `mount` adds them.
18
- * It shows the root again when `baseUrl` changes. A bad `baseUrl` or `getToken` throws. Pass a stable `getToken`, because a new function makes a new client. */
19
- declare function DriveBrowser({ baseUrl, getToken, onOpenItem }: DriveUiOptions): react.JSX.Element | null;
40
+ * It keeps its location when `baseUrl` changes. A bad `baseUrl` or `getToken` throws. Pass a stable `getToken`, because a new function makes a new client. */
41
+ declare function DriveBrowser({ baseUrl, getToken, onOpenItem, location, onNavigate }: DriveUiOptions): react.JSX.Element | null;
20
42
  interface MountedDrive {
21
- /** Show the drive with another address, token callback, or item-open callback. The folder the person has open resets. */
43
+ /** Show the drive with another address, token callback, or item-open callback. The current location stays unless options.location is supplied. */
22
44
  update(options: DriveUiOptions): void;
45
+ /** Select a location without remounting the drive. Does not call onNavigate. */
46
+ setLocation(location: DriveLocation): void;
47
+ /** Current location. A folder path is suitable for SDK registerDocument({ folder }). */
48
+ getLocation(): DriveLocation;
23
49
  /** Remove the UI. A request that runs is aborted, and no later request starts or calls `getToken`. Calling it again does nothing. */
24
50
  unmount(): void;
25
51
  }
26
52
  /** Render the drive into an element, for an app that does not use React itself. A bad `baseUrl` or `getToken` throws a `TypeError`. */
27
53
  declare function mount(element: HTMLElement, options: DriveUiOptions): MountedDrive;
28
54
 
29
- export { DriveBrowser, type DriveUiOptions, Item, type MountedDrive, type OnOpenItem, mount };
55
+ export { DriveBrowser, type DriveLocation, type DriveNavigationOptions, type DriveUiOptions, Item, type MountedDrive, type OnOpenItem, mount };