@mdbase-dev/connect 0.1.0-beta.123 → 0.1.0-beta.125

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.
@@ -1,5 +1,6 @@
1
1
  import { COLLECTION_DESCRIPTION_PROBLEM_CODES, captureConnectOutcome, connectSuccess } from "./outcomes.js";
2
2
  import { withRequestBudget } from "./request-budget.js";
3
+ import { wireCollectionDescription } from "./query-wire.js";
3
4
  /** One collection's description lifecycle; no background observer or persisted schema state. */
4
5
  export class CollectionDescriptionCache {
5
6
  transport;
@@ -43,31 +44,3 @@ export class CollectionDescriptionCache {
43
44
  }), COLLECTION_DESCRIPTION_PROBLEM_CODES).then((outcome) => outcome.ok ? outcome.value : outcome);
44
45
  }
45
46
  }
46
- function wireCollectionDescription(value) {
47
- return {
48
- protocolVersion: value.protocol_version,
49
- collectionId: value.collection_id,
50
- displayName: value.display_name,
51
- specVersion: value.spec_version,
52
- operations: value.operations,
53
- changeCursor: value.change_cursor,
54
- types: value.types,
55
- contracts: value.contracts.map((contract) => ({
56
- contractType: contract.contract_type,
57
- id: contract.id,
58
- version: contract.version,
59
- digest: contract.digest,
60
- schema: contract.schema,
61
- ...(contract.binding_schema ? { bindingSchema: contract.binding_schema } : {}),
62
- implementations: contract.implementations.map((implementation) => ({
63
- typeName: implementation.type_name,
64
- typeVersion: implementation.type_version,
65
- ...(implementation.type_path ? { typePath: implementation.type_path } : {}),
66
- digest: implementation.digest,
67
- fields: implementation.fields,
68
- ...(implementation.binding ? { binding: implementation.binding } : {})
69
- }))
70
- })),
71
- ...(value.configuration ? { configuration: value.configuration } : {})
72
- };
73
- }
@@ -1,4 +1,5 @@
1
1
  import type { CollectionFileDescriptor as WireFile } from "@mdbase-dev/connect-protocol";
2
+ export declare const FILE_ID: RegExp;
2
3
  export interface CollectionFileDescriptor {
3
4
  fileId: string;
4
5
  path: string;
@@ -9,4 +10,5 @@ export interface CollectionFileDescriptor {
9
10
  mediaClass: import("@mdbase-dev/connect-protocol").FileMediaClass;
10
11
  modifiedAt: string;
11
12
  }
13
+ export declare function validFileDescriptor(value: unknown): value is WireFile;
12
14
  export declare function clientFileDescriptor(file: WireFile): CollectionFileDescriptor;
@@ -1,4 +1,21 @@
1
+ import { connectError } from "./errors.js";
2
+ import { SHA256_DIGEST } from "./file-transfer-internals.js";
3
+ // Match files.v1's UUID format (including nil), not crypto transfer-version rules.
4
+ export const FILE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
5
+ export function validFileDescriptor(value) {
6
+ if (!value || typeof value !== "object")
7
+ return false;
8
+ const file = value;
9
+ return typeof file.file_id === "string" && FILE_ID.test(file.file_id)
10
+ && typeof file.path === "string" && typeof file.revision === "string"
11
+ && typeof file.content_digest === "string" && SHA256_DIGEST.test(file.content_digest)
12
+ && Number.isSafeInteger(file.size) && file.size >= 0
13
+ && ["image", "audio", "video", "pdf", "other"].includes(file.media_class)
14
+ && typeof file.modified_at === "string" && (file.media_type === undefined || typeof file.media_type === "string");
15
+ }
1
16
  export function clientFileDescriptor(file) {
17
+ if (!validFileDescriptor(file))
18
+ throw connectError("invalid_operation_response", "The authority returned an invalid file descriptor.");
2
19
  return {
3
20
  fileId: file.file_id,
4
21
  path: file.path,
@@ -0,0 +1,21 @@
1
+ import type { FileCapability, ListFilesPage } from "@mdbase-dev/connect-protocol";
2
+ import type { ConnectRequestOptions } from "./operation-types.js";
3
+ import { type ConnectOutcome } from "./outcomes.js";
4
+ import { type CollectionFileDescriptor } from "./file-descriptor.js";
5
+ export interface MdbaseFileListOptions extends ConnectRequestOptions {
6
+ folder?: string;
7
+ pageSize?: number;
8
+ /** Called while a cold or changed collection builds its verified binary index. */
9
+ onIndexing?: () => void;
10
+ }
11
+ export type MdbaseFileStatTarget = {
12
+ path: string;
13
+ fileId?: never;
14
+ } | {
15
+ path?: never;
16
+ fileId: string;
17
+ };
18
+ export type FileControlRequest = <Result>(method: "GET" | "POST" | "DELETE", path?: string, input?: unknown, signal?: AbortSignal) => Promise<Result>;
19
+ export declare function filePage(request: FileControlRequest, query: URLSearchParams, signal?: AbortSignal, onIndexing?: () => void): Promise<ListFilesPage>;
20
+ export declare function listFiles(capability: () => FileCapability | null, request: FileControlRequest, options: MdbaseFileListOptions, timeoutMs: number | null): AsyncGenerator<CollectionFileDescriptor>;
21
+ export declare function statFile(capability: () => FileCapability | null, request: FileControlRequest, supports: (id: string, options?: ConnectRequestOptions) => Promise<ConnectOutcome<boolean>>, list: (options: MdbaseFileListOptions) => AsyncIterable<CollectionFileDescriptor>, target: MdbaseFileStatTarget, options: ConnectRequestOptions, timeoutMs: number | null): Promise<ConnectOutcome<CollectionFileDescriptor | null>>;
@@ -0,0 +1,112 @@
1
+ import { abortableDelay } from "./async.js";
2
+ import { authorityCapabilities } from "./authority-features.js";
3
+ import { connectError, MdbaseConnectError } from "./errors.js";
4
+ import { normalizeFileError, throwIfAborted, validPageSize } from "./file-transfer-internals.js";
5
+ import { ALL_CONNECT_PROBLEM_CODES, captureConnectOutcome } from "./outcomes.js";
6
+ import { createRequestBudget, withRequestBudget } from "./request-budget.js";
7
+ import { clientFileDescriptor, FILE_ID } from "./file-descriptor.js";
8
+ export async function filePage(request, query, signal, onIndexing) {
9
+ while (true) {
10
+ throwIfAborted(signal);
11
+ try {
12
+ const page = await request("GET", `?${query}`, undefined, signal);
13
+ if (page?.protocol_version !== 1 || page.type !== "files_page" || !Array.isArray(page.files)
14
+ || (page.next !== undefined && (typeof page.next !== "string" || !page.next))) {
15
+ throw connectError("invalid_operation_response", "The authority returned an invalid file page.");
16
+ }
17
+ authorityCapabilities(page.authority_capabilities);
18
+ return page;
19
+ }
20
+ catch (error) {
21
+ const normalized = normalizeFileError(error);
22
+ if (normalized.code !== "file_index_warming")
23
+ throw normalized;
24
+ onIndexing?.();
25
+ await abortableDelay(500, signal);
26
+ }
27
+ }
28
+ }
29
+ export async function* listFiles(capability, request, options, timeoutMs) {
30
+ const budget = createRequestBudget(options, timeoutMs);
31
+ try {
32
+ requireList(capability());
33
+ let after;
34
+ do {
35
+ const query = new URLSearchParams({
36
+ protocol_version: "1",
37
+ ...(options.folder ? { folder: options.folder } : {}),
38
+ ...(after ? { after } : {}),
39
+ ...(options.pageSize ? { limit: String(validPageSize(options.pageSize)) } : {})
40
+ });
41
+ const page = await filePage(request, query, budget.signal, options.onIndexing);
42
+ for (const file of page.files) {
43
+ throwIfAborted(budget.signal);
44
+ yield clientFileDescriptor(file);
45
+ }
46
+ after = page.next;
47
+ } while (after);
48
+ }
49
+ finally {
50
+ budget.dispose();
51
+ }
52
+ }
53
+ export function statFile(capability, request, supports, list, target, options, timeoutMs) {
54
+ return captureConnectOutcome(() => withRequestBudget(options, timeoutMs, async (budget) => {
55
+ requireList(capability());
56
+ if (!target || typeof target !== "object" || Object.keys(target).length !== 1
57
+ || !(Object.hasOwn(target, "path") && typeof target.path === "string"
58
+ || Object.hasOwn(target, "fileId") && typeof target.fileId === "string" && FILE_ID.test(target.fileId))) {
59
+ throw connectError("invalid_request", "File stat requires exactly one path or UUID fileId.");
60
+ }
61
+ // Collection-relative wire syntax only. Eligibility, exclusion rules and
62
+ // filesystem access remain authority-owned, including during legacy listing.
63
+ if (target.path !== undefined && (target.path.length > 1024 || target.path.includes("\\")
64
+ || target.path.split("/").some(part => !part || part.startsWith(".")))) {
65
+ throw connectError("invalid_request", "File stat requires a collection-relative file path.");
66
+ }
67
+ const supported = await supports("files-stat-v1", { signal: budget.signal, timeoutMs: null });
68
+ if (!supported.ok)
69
+ throw new MdbaseConnectError(supported.problem);
70
+ throwIfAborted(budget.signal);
71
+ if (supported.value) {
72
+ const result = await request("POST", "stat", {
73
+ protocol_version: 1, type: "stat_file",
74
+ ...(target.path !== undefined ? { path: target.path } : { file_id: target.fileId })
75
+ }, budget.signal);
76
+ if (result?.protocol_version !== 1 || result.type !== "file_stat" || result.file === undefined) {
77
+ throw connectError("invalid_operation_response", "The authority returned an invalid file stat.");
78
+ }
79
+ if (result.file === null)
80
+ return null;
81
+ const file = clientFileDescriptor(result.file);
82
+ if (target.path !== undefined ? portableKey(file.path) !== portableKey(target.path)
83
+ : file.fileId.toLowerCase() !== target.fileId.toLowerCase()) {
84
+ throw connectError("invalid_operation_response", "The authority returned a stat for a different file.");
85
+ }
86
+ return file;
87
+ }
88
+ // Consumers: Writer/Reader/editor/TaskNotes against late-updated authorities.
89
+ // Remove once the minimum files-stat authority, all consumer pins, and N-1
90
+ // rollback/connection-cache windows close. Listing is not point-cost or atomic.
91
+ const folder = target.path?.includes("/") ? target.path.slice(0, target.path.lastIndexOf("/")) : undefined;
92
+ for await (const file of list({ folder, signal: budget.signal, timeoutMs: null })) {
93
+ throwIfAborted(budget.signal);
94
+ if (target.path !== undefined
95
+ ? portableKey(file.path) === portableKey(target.path)
96
+ : file.fileId.toLowerCase() === target.fileId.toLowerCase())
97
+ return file;
98
+ }
99
+ throwIfAborted(budget.signal);
100
+ return null;
101
+ }), ALL_CONNECT_PROBLEM_CODES);
102
+ }
103
+ function requireList(capability) {
104
+ if (!capability?.actions.includes("list"))
105
+ throw connectError("not_authorized", "This connection is not authorized to list files.");
106
+ }
107
+ // File protocol portable identity comparison, not collection eligibility validation.
108
+ function portableKey(path) {
109
+ // Match Connect's NFC + per-Unicode-scalar lowercase + NFC key, not JS's
110
+ // context-sensitive whole-string lowercase (e.g. Greek final sigma).
111
+ return Array.from(path.normalize("NFC"), scalar => scalar.toLowerCase()).join("").normalize("NFC");
112
+ }
package/dist/files.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type CollectionFileDescriptor } from "./file-descriptor.js";
2
2
  export type { CollectionFileDescriptor } from "./file-descriptor.js";
3
3
  import type { FileCapability, FileTransferSession } from "@mdbase-dev/connect-protocol";
4
+ import { type FileControlRequest, type MdbaseFileListOptions, type MdbaseFileStatTarget } from "./file-metadata.js";
5
+ export type { MdbaseFileListOptions, MdbaseFileStatTarget } from "./file-metadata.js";
6
+ import { type ConnectOutcome } from "./outcomes.js";
4
7
  import { type FileSource } from "./file-transfer-internals.js";
5
8
  import type { ConnectRequestOptions } from "./operation-types.js";
6
9
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
@@ -30,12 +33,6 @@ export interface MdbaseFileProgress {
30
33
  transferredBytes: number;
31
34
  totalBytes: number;
32
35
  }
33
- export interface MdbaseFileListOptions extends ConnectRequestOptions {
34
- folder?: string;
35
- pageSize?: number;
36
- /** Called while a cold or changed collection builds its verified binary index. */
37
- onIndexing?: () => void;
38
- }
39
36
  export interface MdbaseFileUploadOptions extends ConnectRequestOptions {
40
37
  mediaType?: string;
41
38
  ifRevision?: string;
@@ -68,7 +65,6 @@ export interface MdbaseFramedFileTransport {
68
65
  export interface MdbaseHostedFileTransport {
69
66
  downloadPart(session: FileTransferSession, partIndex: number, expectedLength: number, signal?: AbortSignal): Promise<ReadableStream<Uint8Array>>;
70
67
  }
71
- type ControlRequest = <Result>(method: "GET" | "POST" | "DELETE", path?: string, input?: unknown, signal?: AbortSignal) => Promise<Result>;
72
68
  /** Ergonomic facade over resumable, authority-specific file delivery. */
73
69
  export declare class MdbaseFileClient {
74
70
  private readonly capability;
@@ -76,8 +72,11 @@ export declare class MdbaseFileClient {
76
72
  private readonly framed?;
77
73
  private readonly hosted?;
78
74
  private readonly timeouts;
79
- constructor(capability: () => FileCapability | null, request: ControlRequest, framed?: MdbaseFramedFileTransport | undefined, hosted?: MdbaseHostedFileTransport | undefined, timeouts?: ResolvedConnectTimeouts);
75
+ private readonly supportsAuthorityFeature;
76
+ constructor(capability: () => FileCapability | null, request: FileControlRequest, framed?: MdbaseFramedFileTransport | undefined, hosted?: MdbaseHostedFileTransport | undefined, timeouts?: ResolvedConnectTimeouts, supportsAuthorityFeature?: (id: string, options?: ConnectRequestOptions) => Promise<ConnectOutcome<boolean>>);
80
77
  list(options?: MdbaseFileListOptions): AsyncGenerator<CollectionFileDescriptor>;
78
+ /** Current metadata lookup; requires file list approval, not file byte access. */
79
+ stat(target: MdbaseFileStatTarget, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionFileDescriptor | null>>;
81
80
  upload(path: string, source: MdbaseFileSource, options?: MdbaseFileUploadOptions): Promise<CollectionFileDescriptor>;
82
81
  private uploadWithinBudget;
83
82
  /**
package/dist/files.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { clientFileDescriptor } from "./file-descriptor.js";
2
2
  import { FILE_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
3
- import { abortableDelay } from "./async.js";
3
+ import { listFiles, statFile } from "./file-metadata.js";
4
+ import { connectSuccess } from "./outcomes.js";
4
5
  import { MdbaseConnectError, connectError } from "./errors.js";
5
6
  import { IncrementalSha256 } from "./file-sha256.js";
6
7
  import { BinaryPartReader } from "./file-stream-source.js";
7
- import { MAX_BUFFERED_DOWNLOAD_BYTES, MAX_OBJECT_ATTEMPTS, SHA256_DIGEST, UUID, browserObjectHeaders, chunkLength, hashBlob, mapConcurrent, normalizeFileError, requirePreparedPart, requireTransferSession, requireTransferStatus, retryChunk, sourceBlob, throwIfAborted, uploadBodyBytes, uploadBodyLength, validConcurrency, validPageSize } from "./file-transfer-internals.js";
8
- import { createRequestBudget, resolveConnectTimeouts, withCooperativeRequestBudget } from "./request-budget.js";
8
+ import { MAX_BUFFERED_DOWNLOAD_BYTES, MAX_OBJECT_ATTEMPTS, SHA256_DIGEST, UUID, browserObjectHeaders, chunkLength, hashBlob, mapConcurrent, normalizeFileError, requirePreparedPart, requireTransferSession, requireTransferStatus, retryChunk, sourceBlob, throwIfAborted, uploadBodyBytes, uploadBodyLength, validConcurrency } from "./file-transfer-internals.js";
9
+ import { resolveConnectTimeouts, withCooperativeRequestBudget } from "./request-budget.js";
9
10
  /** Ergonomic facade over resumable, authority-specific file delivery. */
10
11
  export class MdbaseFileClient {
11
12
  capability;
@@ -13,54 +14,21 @@ export class MdbaseFileClient {
13
14
  framed;
14
15
  hosted;
15
16
  timeouts;
16
- constructor(capability, request, framed, hosted, timeouts = resolveConnectTimeouts()) {
17
+ supportsAuthorityFeature;
18
+ constructor(capability, request, framed, hosted, timeouts = resolveConnectTimeouts(), supportsAuthorityFeature = async () => connectSuccess(false)) {
17
19
  this.capability = capability;
18
20
  this.request = request;
19
21
  this.framed = framed;
20
22
  this.hosted = hosted;
21
23
  this.timeouts = timeouts;
24
+ this.supportsAuthorityFeature = supportsAuthorityFeature;
22
25
  }
23
- async *list(options = {}) {
24
- const budget = createRequestBudget(options, this.timeouts.fileIndexMs);
25
- const signal = budget.signal;
26
- try {
27
- this.requireAction("list");
28
- let after;
29
- do {
30
- throwIfAborted(signal);
31
- const query = new URLSearchParams({
32
- protocol_version: String(FILE_PROTOCOL_VERSION),
33
- ...(options.folder ? { folder: options.folder } : {}),
34
- ...(after ? { after } : {}),
35
- ...(options.pageSize ? { limit: String(validPageSize(options.pageSize)) } : {})
36
- });
37
- let page;
38
- while (true) {
39
- try {
40
- page = await this.request("GET", `?${query.toString()}`, undefined, signal);
41
- break;
42
- }
43
- catch (error) {
44
- const normalized = normalizeFileError(error);
45
- if (normalized.code !== "file_index_warming")
46
- throw normalized;
47
- options.onIndexing?.();
48
- await abortableDelay(500, signal);
49
- }
50
- }
51
- if (page.protocol_version !== 1 || page.type !== "files_page" || !Array.isArray(page.files)) {
52
- throw connectError("invalid_operation_response", "The authority returned an invalid file page.");
53
- }
54
- for (const file of page.files) {
55
- throwIfAborted(signal);
56
- yield clientFileDescriptor(file);
57
- }
58
- after = page.next;
59
- } while (after);
60
- }
61
- finally {
62
- budget.dispose();
63
- }
26
+ list(options = {}) {
27
+ return listFiles(this.capability, this.request, options, this.timeouts.fileIndexMs);
28
+ }
29
+ /** Current metadata lookup; requires file list approval, not file byte access. */
30
+ stat(target, options = {}) {
31
+ return statFile(this.capability, this.request, this.supportsAuthorityFeature, options => this.list(options), target, options, this.timeouts.fileIndexMs);
64
32
  }
65
33
  async upload(path, source, options = {}) {
66
34
  return withCooperativeRequestBudget(options, this.timeouts.uploadMs, (budget) => this.uploadWithinBudget(path, source, { ...options, signal: budget.signal, timeoutMs: null }));
package/dist/index.d.ts CHANGED
@@ -19,11 +19,11 @@ export type { MdbaseRecordResolution, MdbaseRecordSessionOptions, MdbaseRecordSe
19
19
  export { MdbasePeopleClient, PERSON_CONTRACT, sameIdentity } from "./people-client.js";
20
20
  export type { CurrentAccount, InvalidPersonRecord, PeopleDirectory, PeopleDirectoryOptions, PersonRecord, PersonResolution } from "./people-client.js";
21
21
  export type { AccountIdentity, AccountProfile, CollectionMemberProfile, ApplicationPeopleRequirement, PeoplePermission } from "@mdbase-dev/connect-protocol";
22
- export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceipt, MdbaseFileStreamSource, MdbaseFileProgress, MdbaseFileListOptions, MdbaseFileUploadOptions, MdbaseFileStreamUploadOptions, MdbaseFileDownloadOptions, MdbaseFileMoveOptions, MdbaseFileDeleteOptions } from "./files.js";
22
+ export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceipt, MdbaseFileStreamSource, MdbaseFileProgress, MdbaseFileListOptions, MdbaseFileStatTarget, MdbaseFileUploadOptions, MdbaseFileStreamUploadOptions, MdbaseFileDownloadOptions, MdbaseFileMoveOptions, MdbaseFileDeleteOptions } from "./files.js";
23
23
  export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
24
24
  export type { MdbaseNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNativeNotificationRegistration, MdbaseNativeNotificationData, MdbasePushPayload } from "./notifications.js";
25
25
  export type { ConnectSuccess, ConnectFailure, ConnectOutcome, CommonOperationProblemCode, RegistrationProblemCode, AuthorizationProblemCode, NotificationProblemCode, DirectAccessProblemCode, SessionProblemCode, CollectionSetupProblemCode, CollectionDescriptionProblemCode, CollectionReadProblemCode, CollectionQueryProblemCode, CollectionMutationProblemCode, CollectionChangesProblemCode, CollectionTypeProblemCode } from "./outcomes.js";
26
- export type { MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ReadInput, ReadManyOptions, ReadManyRecord, ReadManyEntry, ReadManyBatchError, ReadManyResult, DataContractSelector, CollectionFileMetadata, DataContractViewIdentity, QueryRecord, RecordDocument, QueryInput, QueryProjection, QuerySelectionExpression, QueryOrder, QuerySummary, QueryResult, QueryPagesOptions, QueryAllOptions, QueryPage, ConnectRequestOptions, MutationEstimate, MutationProgressState, MutationProgress, MutationProgressOptions, RequestCoordinationOptions, RenameProgressOptions, DeleteProgressOptions, PendingMutationSummary, PendingMutation, CreateInput, UpdateInput, DeleteInput, DeleteResult, DeletePreflightResult, RenameInput, RenameResult, RenamePreflightResult, ReadTypeInput, CreateTypeInput, UpdateTypeInput, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, SavedViewPresentation, SavedViewSource, SavedViewProperty, SavedNamedView, SavedViewDocument, SavedViewList, SavedViewSourceDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, ContractSetupChoice, TypePackResourceDiff, TypePackReceipt, TypePackAssessment, TypePackApplyResult, ConfigurationContributionValue, ConfigurationRequirement, ConfigurationProvision, ApplicationCollectionSetupRequirements, ApplicationCollectionSetupProvisions, ConfigurationSetupConflict, ConfigurationSetupAssessment, CollectionSetupAssessment, CollectionSetupReceipt, CollectionSetupApplyResult, AssessTypePackInput, ApplyTypePackInput, AssessCollectionSetupInput, ApplyCollectionSetupInput, ChangesInput, WatchOptions, WatchRetryOptions, WatchStatus, WatchInput, MdbaseWatchSubscription, CollectionChange, RecordChangeMetadata, DescribeOptions, CollectionChangesPage, CollectionDescription, CollectionContractDescriptor, CollectionContractImplementationDescriptor, CollectionTypeDescriptor } from "./operation-types.js";
26
+ export type { MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ReadInput, ReadManyOptions, ReadManyRecord, ReadManyEntry, ReadManyBatchError, ReadManyResult, DataContractSelector, CollectionFileMetadata, DataContractViewIdentity, QueryRecord, RecordDocument, QueryInput, QueryMetadataInput, QueryMetadataRecord, QueryMetadataResult, QueryMetadataPage, QueryProjection, QuerySelectionExpression, QueryOrder, QuerySummary, QueryResult, QueryPagesOptions, QueryAllOptions, QueryPage, ConnectRequestOptions, MutationEstimate, MutationProgressState, MutationProgress, MutationProgressOptions, RequestCoordinationOptions, RenameProgressOptions, DeleteProgressOptions, PendingMutationSummary, PendingMutation, CreateInput, UpdateInput, DeleteInput, DeleteResult, DeletePreflightResult, RenameInput, RenameResult, RenamePreflightResult, ReadTypeInput, CreateTypeInput, UpdateTypeInput, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, SavedViewPresentation, SavedViewSource, SavedViewProperty, SavedNamedView, SavedViewDocument, SavedViewList, SavedViewSourceDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, ContractSetupChoice, TypePackResourceDiff, TypePackReceipt, TypePackAssessment, TypePackApplyResult, ConfigurationContributionValue, ConfigurationRequirement, ConfigurationProvision, ApplicationCollectionSetupRequirements, ApplicationCollectionSetupProvisions, ConfigurationSetupConflict, ConfigurationSetupAssessment, CollectionSetupAssessment, CollectionSetupReceipt, CollectionSetupApplyResult, AssessTypePackInput, ApplyTypePackInput, AssessCollectionSetupInput, ApplyCollectionSetupInput, ChangesInput, WatchOptions, WatchRetryOptions, WatchStatus, WatchInput, MdbaseWatchSubscription, CollectionChange, RecordChangeMetadata, DescribeOptions, CollectionChangesPage, CollectionDescription, CollectionContractDescriptor, CollectionContractImplementationDescriptor, CollectionTypeDescriptor } from "./operation-types.js";
27
27
  export { linksTo } from "./query-predicates.js";
28
28
  export type { LinksToOptions } from "./query-predicates.js";
29
29
  export { externalStore } from "./external-store.js";
@@ -1,4 +1,4 @@
1
- import { decodeFileFrame, FILE_PROTOCOL_VERSION, FILE_TRANSFER_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
1
+ import { decodeFileFrame, isMutatingOperation, FILE_PROTOCOL_VERSION, FILE_TRANSFER_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
2
2
  import { decryptRelayResponse, encryptRelayRequest } from "./crypto.js";
3
3
  import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
4
4
  import { GrantFileTransferCipher } from "./file-crypto.js";
@@ -51,7 +51,7 @@ export class LocalFileTransport {
51
51
  if (response)
52
52
  this.options.onDirectUnavailable();
53
53
  }
54
- if (triedDirect && method === "GET") {
54
+ if (triedDirect && !isMutatingOperation("file_control", controlInput)) {
55
55
  token = token.expiresAt > Date.now() + 30_000
56
56
  ? token
57
57
  : await this.options.refreshAuthorization(signal);
@@ -81,7 +81,7 @@ export class LocalFileTransport {
81
81
  return this.controlAttempt(token, method, path, input, signal, freshReadRetried, connectorBusyRetries + 1, leases);
82
82
  }
83
83
  if (error.code === "fresh_request_required"
84
- && method === "GET"
84
+ && !isMutatingOperation("file_control", localFileControlInput(method, path, input))
85
85
  && !freshReadRetried) {
86
86
  return this.controlAttempt(token, method, path, input, signal, true, connectorBusyRetries, leases);
87
87
  }
@@ -94,7 +94,7 @@ export class LocalFileTransport {
94
94
  const decrypted = await decryptRelayResponse(this.options.keyStore, token.keyHandle, relayBinding(token), encryptedRequest, encryptedResponse);
95
95
  if (!decrypted.ok
96
96
  && decrypted.problem.code === "fresh_request_required"
97
- && method === "GET"
97
+ && !isMutatingOperation("file_control", localFileControlInput(method, path, input))
98
98
  && !freshReadRetried) {
99
99
  return this.controlAttempt(token, method, path, input, signal, true, connectorBusyRetries, leases);
100
100
  }
@@ -407,19 +407,21 @@ function localFileControlInput(method, path, input) {
407
407
  return transferControl("abort_file_transfer", segments[1]);
408
408
  }
409
409
  if (method === "POST" && isRecord(input)) {
410
- const expectedType = path === "uploads"
411
- ? "open_file_upload"
412
- : path === "downloads"
413
- ? "open_file_download"
414
- : segments.length === 3 && segments[0] === "uploads" && segments[2] === "commit"
415
- ? "commit_file_upload"
416
- : segments.length === 2 && segments[1] === "move"
417
- && input.file_id === decodeURIComponent(segments[0])
418
- ? "move_file"
419
- : segments.length === 2 && segments[1] === "delete"
410
+ const expectedType = path === "stat"
411
+ ? "stat_file"
412
+ : path === "uploads"
413
+ ? "open_file_upload"
414
+ : path === "downloads"
415
+ ? "open_file_download"
416
+ : segments.length === 3 && segments[0] === "uploads" && segments[2] === "commit"
417
+ ? "commit_file_upload"
418
+ : segments.length === 2 && segments[1] === "move"
420
419
  && input.file_id === decodeURIComponent(segments[0])
421
- ? "delete_file"
422
- : null;
420
+ ? "move_file"
421
+ : segments.length === 2 && segments[1] === "delete"
422
+ && input.file_id === decodeURIComponent(segments[0])
423
+ ? "delete_file"
424
+ : null;
423
425
  if (expectedType && input.type === expectedType)
424
426
  return { ...input };
425
427
  }
@@ -53,6 +53,8 @@ export interface DataContractViewIdentity {
53
53
  }
54
54
  export interface QueryRecord<Frontmatter extends JsonObject = JsonObject> {
55
55
  path: string;
56
+ /** Exact-source token when the authority supports query-record-revisions-v1. */
57
+ revision?: string;
56
58
  frontmatter?: Frontmatter;
57
59
  effectiveFrontmatter?: Frontmatter;
58
60
  body?: string;
@@ -64,17 +66,14 @@ export interface QueryRecord<Frontmatter extends JsonObject = JsonObject> {
64
66
  values?: JsonObject;
65
67
  contract?: DataContractViewIdentity;
66
68
  }
67
- /** Query-shaped batched record today; a future authority may supply a revision. */
68
- export interface ReadManyRecord<Frontmatter extends JsonObject = JsonObject> extends QueryRecord<Frontmatter> {
69
- /** Absent on current query authorities. Never synthesized from timestamps or content. */
70
- revision?: string;
71
- }
69
+ /** Query-shaped projection; qualified document batches supply exact-source revisions. */
70
+ export type ReadManyRecord<Frontmatter extends JsonObject = JsonObject> = QueryRecord<Frontmatter>;
72
71
  export interface ReadManyOptions extends ConnectRequestOptions {
73
72
  includeBody?: boolean;
74
73
  frontmatterMode?: QueryInput["frontmatterMode"];
75
74
  /** Restrict matching records to these raw collection types, not a contract view. */
76
75
  types?: string[];
77
- /** Unique paths per independent query, default 100; range 1..1,000. */
76
+ /** Unique paths per batch, default 100; range 1..1,000. Document authorities cap at 100. */
78
77
  batchSize?: number;
79
78
  /** Independent batches in flight, default 4; range 1..4. Cursor pages stay serial. */
80
79
  concurrency?: number;
@@ -99,7 +98,7 @@ export interface ReadManyBatchError {
99
98
  export interface ReadManyResult<Frontmatter extends JsonObject = JsonObject> {
100
99
  /** One entry per input path, in input order, including duplicates. */
101
100
  results: ReadManyEntry<Frontmatter>[];
102
- /** A failed batch is not proof of missing records; its entries are errors. */
101
+ /** Batch failures cover the entire batch; semantic item failures cover only their paths. */
103
102
  errors: ReadManyBatchError[];
104
103
  }
105
104
  export interface RecordDocument<Frontmatter extends JsonObject = JsonObject> {
@@ -145,6 +144,8 @@ export interface CollectionContractDescriptor {
145
144
  implementations: CollectionContractImplementationDescriptor[];
146
145
  }
147
146
  export interface CollectionDescription {
147
+ /** Immutable authority implementation features; never a permission claim. */
148
+ authorityCapabilities?: readonly string[];
148
149
  protocolVersion: 1;
149
150
  collectionId: string;
150
151
  displayName: string;
@@ -177,6 +178,7 @@ export interface QuerySummary {
177
178
  }
178
179
  /** Application-facing form of the canonical mdbase v0.3 query schema. */
179
180
  export interface QueryInput {
181
+ output?: never;
180
182
  /**
181
183
  * Queries using a semantic contract view accept only `types`, `timezone`,
182
184
  * pagination, `frontmatterMode`, and `contract`; filter normalized fields in
@@ -210,8 +212,26 @@ export interface QueryInput {
210
212
  /** Select one exact semantic contract/provider view for this query. */
211
213
  contract?: DataContractSelector;
212
214
  }
213
- export interface QueryResult<Record extends JsonObject = JsonObject> {
214
- results: Array<QueryRecord<Record>>;
215
+ export type QueryMetadataInput = Omit<QueryInput, "output" | "includeBody"> & {
216
+ output: "metadata";
217
+ includeBody?: false;
218
+ };
219
+ /** Partial metadata is not a record document; revision and selected values are required. */
220
+ export interface QueryMetadataRecord {
221
+ path: string;
222
+ types: string[];
223
+ revision: string;
224
+ values: JsonObject;
225
+ contract?: DataContractViewIdentity;
226
+ }
227
+ export type QueryMetadataResult = QueryResult<JsonObject, QueryMetadataRecord> & {
228
+ output: "metadata";
229
+ };
230
+ export type QueryMetadataPage = QueryPage<JsonObject, QueryMetadataRecord> & {
231
+ output: "metadata";
232
+ };
233
+ export interface QueryResult<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> {
234
+ results: Row[];
215
235
  meta?: {
216
236
  /** Exact total when it is available within the page summary budget. */
217
237
  totalCount?: number;
@@ -226,7 +246,7 @@ export interface QueryResult<Record extends JsonObject = JsonObject> {
226
246
  snapshot?: string;
227
247
  };
228
248
  }
229
- export interface QueryPagesOptions<Record extends JsonObject = JsonObject> {
249
+ export interface QueryPagesOptions<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> {
230
250
  /** Initial page size. Cursor continuations use this pinned size. */
231
251
  firstPageSize?: number;
232
252
  /** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
@@ -237,19 +257,19 @@ export interface QueryPagesOptions<Record extends JsonObject = JsonObject> {
237
257
  /** Independent budget for each page requested by this caller-driven iterator. */
238
258
  pageTimeoutMs?: number | null;
239
259
  coordination?: RequestCoordinationOptions;
240
- onProgress?: (page: QueryPage<Record>) => void;
260
+ onProgress?: (page: QueryPage<Record, Row>) => void;
241
261
  }
242
- export interface QueryAllOptions<Record extends JsonObject = JsonObject> extends ConnectRequestOptions {
262
+ export interface QueryAllOptions<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> extends ConnectRequestOptions {
243
263
  /** Initial page size. Cursor continuations use this pinned size. */
244
264
  firstPageSize?: number;
245
265
  /** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
246
266
  pageSize?: number;
247
267
  /** Total rows to return, independent of page size. */
248
268
  maxResults?: number;
249
- onProgress?: (page: QueryPage<Record>) => void;
269
+ onProgress?: (page: QueryPage<Record, Row>) => void;
250
270
  }
251
- export interface QueryPage<Record extends JsonObject = JsonObject> {
252
- results: QueryResult<Record>["results"];
271
+ export interface QueryPage<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> {
272
+ results: QueryResult<Record, Row>["results"];
253
273
  meta?: QueryResult<Record>["meta"];
254
274
  page: number;
255
275
  offset: number;
@@ -554,6 +574,11 @@ export interface TypePackResourceDiff {
554
574
  currentDigest?: string;
555
575
  installedDigest?: string;
556
576
  adoptedFromDigest?: string;
577
+ /** The declared baseline an engine upgraded this seed from (seed `update` only). */
578
+ upgradeBaseline?: {
579
+ digest: string;
580
+ version?: number;
581
+ };
557
582
  reason?: string;
558
583
  }
559
584
  export interface TypePackReceipt {
@@ -561,7 +586,10 @@ export interface TypePackReceipt {
561
586
  version: string;
562
587
  digest: string;
563
588
  installedBy: string;
564
- resources: Array<Omit<TypePackResourceDiff, "action" | "currentDigest" | "installedDigest" | "reason">>;
589
+ resources: Array<Omit<TypePackResourceDiff, "action" | "currentDigest" | "installedDigest" | "upgradeBaseline" | "reason"> & {
590
+ /** Seed only: the publisher document the target descends from, when known. */
591
+ originDigest?: string;
592
+ }>;
565
593
  }
566
594
  export interface TypePackAssessment {
567
595
  status: "current" | "install" | "upgrade" | "downgrade" | "reconfigure" | "conflict";
@@ -1,6 +1,10 @@
1
1
  import type { JsonObject } from "@mdbase-dev/connect-protocol";
2
2
  import { type CollectionQueryProblemCode, type ConnectOutcome } from "./outcomes.js";
3
- import type { ConnectRequestOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult } from "./operation-types.js";
4
- type QueryOperation<Frontmatter extends JsonObject> = (input: QueryInput, options?: ConnectRequestOptions) => Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
5
- export declare function coordinatedQueryPages<Frontmatter extends JsonObject>(query: QueryOperation<Frontmatter>, releaseQueryCursor: (cursor: string) => Promise<void>, input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
3
+ import type { ConnectRequestOptions, QueryInput, QueryMetadataInput, QueryRecord, QueryPage, QueryPagesOptions, QueryResult } from "./operation-types.js";
4
+ type QueryOperation<Frontmatter extends JsonObject, Row> = (input: QueryInput | QueryMetadataInput, options?: ConnectRequestOptions) => Promise<ConnectOutcome<QueryResult<Frontmatter, Row> & {
5
+ output?: "metadata";
6
+ }, CollectionQueryProblemCode>>;
7
+ export declare function coordinatedQueryPages<Frontmatter extends JsonObject, Row = QueryRecord<Frontmatter>>(query: QueryOperation<Frontmatter, Row>, releaseQueryCursor: (cursor: string) => Promise<void>, input?: QueryInput | QueryMetadataInput, options?: QueryPagesOptions<Frontmatter, Row>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter, Row> & {
8
+ output?: "metadata";
9
+ }, CollectionQueryProblemCode>>;
6
10
  export {};
@@ -25,7 +25,8 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
25
25
  const automaticCursorProbe = pageNumber === 0
26
26
  && requestedCursor === undefined
27
27
  && requestedPagination === undefined
28
- && requestedSnapshot === undefined;
28
+ && requestedSnapshot === undefined
29
+ && input.output !== "metadata";
29
30
  let queried = await query({
30
31
  ...criteria,
31
32
  // Initial size is portable across authorities; continuations use the
@@ -96,6 +97,7 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
96
97
  loaded += results.length;
97
98
  const complete = loaded >= maxResults || !result.meta?.hasMore || result.results.length === 0;
98
99
  const page = {
100
+ ...(result.output ? { output: result.output } : {}),
99
101
  results,
100
102
  ...(result.meta ? { meta: result.meta } : {}),
101
103
  page: pageNumber,