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 +13 -0
- package/README.md +91 -2
- package/dist/account.d.ts +1 -1
- package/dist/account.js +2 -2
- package/dist/{chunk-W27ZQYPT.js → chunk-6TPC3BUW.js} +1 -1
- package/dist/{chunk-DB4F5EWD.js → chunk-FTIWCBCI.js} +98 -9
- package/dist/{chunk-HJXRMOUY.js → chunk-NPJG2MYP.js} +1 -1
- package/dist/{chunk-ECVUDAHL.js → chunk-OHDNQ23Q.js} +1 -1
- package/dist/{chunk-SEHT5Q5L.js → chunk-TGOARFYZ.js} +1 -0
- package/dist/cli.js +3 -3
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/{drive-CS1CLfqo.d.ts → drive-BncyFxBL.d.ts} +18 -4
- package/dist/embed.d.ts +31 -5
- package/dist/embed.js +93 -33
- package/dist/index.d.ts +10 -3
- package/dist/index.js +4 -4
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +3 -3
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +2 -2
- package/llms.txt +25 -3
- package/package.json +2 -2
- package/ui-dist/assets/index-CSHYaonS.js +49 -0
- package/ui-dist/index.html +1 -1
- package/ui-dist/assets/index-DqkvcDrx.js +0 -49
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
package/dist/account.js
CHANGED
|
@@ -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
|
|
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)
|
|
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,
|
|
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) => {
|
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-
|
|
7
|
+
} from "./chunk-6TPC3BUW.js";
|
|
8
8
|
import {
|
|
9
9
|
buildExport,
|
|
10
10
|
upload
|
|
11
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
2
|
-
export { c as ActivityAction } from './drive-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import * as agentic_identity from 'agentic-identity';
|
|
2
|
-
import { PrincipalKind, Store, Blobs, PrincipalContext,
|
|
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" | "
|
|
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,
|
|
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-
|
|
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
|
|
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
|
|
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 };
|