@peers-app/peers-sdk 0.30.1 → 0.30.3

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.
@@ -14,7 +14,7 @@ export interface IChunkMigrationResult {
14
14
  files: number;
15
15
  /** Distinct chunks those rows list, less the skipped ones. */
16
16
  needed: number;
17
- /** Chunks only orphaned legacy bundle rows list, which aren't copied. */
17
+ /** Chunks only orphaned runtime bundle rows list, which aren't copied. */
18
18
  skippedBundleChunks: number;
19
19
  /** Needed chunks the store already had. */
20
20
  alreadyStored: number;
@@ -35,6 +35,86 @@ export interface IChunkMigrationResult {
35
35
  * `verified`.
36
36
  */
37
37
  export type ChunkMigrationState = "not-needed" | "pending" | "running" | "blocked" | "verified";
38
+ /** Why a file is retained, or where it is in its local retention window. */
39
+ export type FileCollectionState = "kept" | "referenced" | "never" | "waiting" | "expired" | "unindexed" | "disabled";
40
+ /** Ordinary-file retention on this device; runtime bundles always use grace alone. */
41
+ export type FileRetention = number | "never";
42
+ /** One context's part of a collection plan; chunk totals may overlap other contexts. */
43
+ export interface IFileCollectionContextResult {
44
+ dataContextId: string;
45
+ disabled: boolean;
46
+ retentionMs: FileRetention;
47
+ files: number;
48
+ expiredFiles: number;
49
+ /** Chunks/bytes eligible in this plan, including shared expired chunks; a concurrent put can keep them longer. */
50
+ chunksEligible: number;
51
+ bytesEligible: number;
52
+ rowsDeleted: number;
53
+ /** Next file deadline after this run's planning time. */
54
+ nextExpirationAt?: number;
55
+ /** Next chunk age deadline among expired files (deletion requires strictly older). */
56
+ nextChunkAt?: number;
57
+ }
58
+ /** A collection run. Dry runs report eligible chunks/bytes and bundle rows in the deletion totals, but delete nothing. */
59
+ export interface IFileCollectionResult {
60
+ state: "completed" | "skipped" | "stopped";
61
+ dryRun: boolean;
62
+ reason?: string;
63
+ contexts: IFileCollectionContextResult[];
64
+ files: number;
65
+ expiredFiles: number;
66
+ liveChunks: number;
67
+ chunksFreed: number;
68
+ bytesFreed: number;
69
+ rowsDeleted: number;
70
+ problems: string[];
71
+ startedAt: number;
72
+ finishedAt: number;
73
+ }
74
+ /** A file in a paged, device-local list. */
75
+ export interface IListedFile {
76
+ fileId: string;
77
+ name: string;
78
+ fileSize: number;
79
+ mimeType: string;
80
+ kept: boolean;
81
+ legacy: boolean;
82
+ /** Runtime-owned bundle, whether saved by an older or current installer. */
83
+ runtimeBundle: boolean;
84
+ referenceCount: number;
85
+ referenceTables: string[];
86
+ chunks: "all" | "some" | "none";
87
+ unreferencedAt?: number;
88
+ state: FileCollectionState;
89
+ /** Earliest collection deadline here; shared chunks and the chunk age floor can keep bytes longer. */
90
+ expiresAt?: number;
91
+ /** Why there is no deadline, when applicable. */
92
+ retainedReason?: string;
93
+ }
94
+ /** Context totals, counting logical file bytes (shared content counts for each file). */
95
+ export interface IFileListSummary {
96
+ files: number;
97
+ bytes: number;
98
+ states: Record<FileCollectionState, number>;
99
+ retentionMs: FileRetention;
100
+ }
101
+ /** Keyset paging in file-id order. */
102
+ export interface IListFilesInput {
103
+ after?: string;
104
+ /** Page size, 1–200; defaults to 50. */
105
+ limit?: number;
106
+ }
107
+ /** One page and context totals, with a cursor only when more rows remain. */
108
+ export interface IListFilesResult {
109
+ files: IListedFile[];
110
+ nextCursor?: string;
111
+ summary: IFileListSummary;
112
+ }
113
+ /** Forced reindex outcome for every loaded context. */
114
+ export interface IFileReindexResult {
115
+ contexts: number;
116
+ problems: string[];
117
+ }
38
118
  /** This device's chunk storage for the signed-in account. */
39
119
  export interface IFileStorageStatus {
40
120
  /** The account's chunk store. */
@@ -48,6 +128,12 @@ export interface IFileStorageStatus {
48
128
  /** Accounts on this device still to migrate; it can be removed once none are left. */
49
129
  waitingAccounts: string[];
50
130
  };
131
+ /** Collection's state in this session and its next scheduled run. */
132
+ collection?: {
133
+ state: "idle" | "running" | "skipped" | "stopped";
134
+ lastRun?: IFileCollectionResult;
135
+ nextRunAt?: number;
136
+ };
51
137
  /** The migration of the legacy store. */
52
138
  migration: {
53
139
  state: ChunkMigrationState;
@@ -60,6 +146,14 @@ export interface IFileStorageConsumer {
60
146
  tools: {
61
147
  /** The chunk store's size, the legacy store, and where the migration stands. */
62
148
  getStatus(): Promise<IFileStorageStatus>;
149
+ /** Free expired files' unshared chunks, or report what would be freed now. */
150
+ collect(input?: {
151
+ dryRun?: boolean;
152
+ }): Promise<IFileCollectionResult>;
153
+ /** Page through the caller's context, with its summary. */
154
+ listFiles(input?: IListFilesInput): Promise<IListFilesResult>;
155
+ /** Rebuild every loaded context's file references after raw SQL edits. */
156
+ reindex(): Promise<IFileReindexResult>;
63
157
  /** Copy this account's chunks out of the legacy store now, and verify them. */
64
158
  migrate(): Promise<IChunkMigrationResult>;
65
159
  /** Copy again, then delete the legacy store once every account on the device has migrated. */
@@ -70,7 +164,7 @@ export interface IFileStorageConsumer {
70
164
  };
71
165
  }
72
166
  /**
73
- * Return the System File Storage v1 declaration: this device's chunk store and the migration
74
- * of its legacy chunks. Only the user's own screens and CLI on this device may call it.
167
+ * Return System File Storage v1: local collection, file inspection, reference reindexing and
168
+ * legacy migration. Only the user's own screens and CLI on this device may call it.
75
169
  */
76
170
  export declare function getFileStorageContractDefinition(): IContractDefinition;
@@ -23,8 +23,8 @@ const migrationResultFields = [
23
23
  field("finishedAt", field_type_1.FieldType.number, "When it finished."),
24
24
  ];
25
25
  /**
26
- * Return the System File Storage v1 declaration: this device's chunk store and the migration
27
- * of its legacy chunks. Only the user's own screens and CLI on this device may call it.
26
+ * Return System File Storage v1: local collection, file inspection, reference reindexing and
27
+ * legacy migration. Only the user's own screens and CLI on this device may call it.
28
28
  */
29
29
  function getFileStorageContractDefinition() {
30
30
  return {
@@ -32,7 +32,7 @@ function getFileStorageContractDefinition() {
32
32
  version: exports.FILE_STORAGE_CONTRACT_VERSION,
33
33
  devTag: "dev",
34
34
  name: "System File Storage",
35
- description: "This device's chunk store and the migration of its legacy chunks.",
35
+ description: "This device's file storage, collection and legacy migration.",
36
36
  tables: [],
37
37
  observables: [],
38
38
  events: [],
@@ -47,6 +47,54 @@ function getFileStorageContractDefinition() {
47
47
  optional: true,
48
48
  }),
49
49
  field("migration", field_type_1.FieldType.object, "The migration's state and last run."),
50
+ field("collection", field_type_1.FieldType.object, "Collection's state, last run and next run.", {
51
+ optional: true,
52
+ }),
53
+ ],
54
+ },
55
+ {
56
+ name: "collect",
57
+ usageDescription: "Free expired files' unshared chunks on this device, or preview collection.",
58
+ inputFields: [
59
+ field("dryRun", field_type_1.FieldType.boolean, "Report only; delete nothing.", { optional: true }),
60
+ ],
61
+ outputFields: [
62
+ field("state", field_type_1.FieldType.string, "completed, skipped, or stopped."),
63
+ field("dryRun", field_type_1.FieldType.boolean, "Whether this was a preview."),
64
+ field("reason", field_type_1.FieldType.string, "Why it skipped or stopped.", { optional: true }),
65
+ ...["contexts", "problems"].map((name) => field(name, field_type_1.FieldType.any, name)),
66
+ ...[
67
+ "files",
68
+ "expiredFiles",
69
+ "liveChunks",
70
+ "chunksFreed",
71
+ "bytesFreed",
72
+ "rowsDeleted",
73
+ "startedAt",
74
+ "finishedAt",
75
+ ].map((name) => field(name, field_type_1.FieldType.number, name)),
76
+ ],
77
+ },
78
+ {
79
+ name: "listFiles",
80
+ usageDescription: "List this context's files and their local collection deadlines, in pages.",
81
+ inputFields: [
82
+ field("after", field_type_1.FieldType.string, "Continue after this file id.", { optional: true }),
83
+ field("limit", field_type_1.FieldType.number, "Page size, 1–200; default 50.", { optional: true }),
84
+ ],
85
+ outputFields: [
86
+ field("files", field_type_1.FieldType.any, "The page of files."),
87
+ field("nextCursor", field_type_1.FieldType.string, "Continue with this cursor.", { optional: true }),
88
+ field("summary", field_type_1.FieldType.object, "Context counts, logical bytes and retention."),
89
+ ],
90
+ },
91
+ {
92
+ name: "reindex",
93
+ usageDescription: "Rebuild file references in every loaded context after raw SQL edits.",
94
+ inputFields: [],
95
+ outputFields: [
96
+ field("contexts", field_type_1.FieldType.number, "Contexts reindexed."),
97
+ field("problems", field_type_1.FieldType.any, "Tables that could not be opened or indexed."),
50
98
  ],
51
99
  },
52
100
  {
@@ -0,0 +1,148 @@
1
+ /** Trigger for an overlapping, at most five-minute connectivity measurement. */
2
+ export type MeshEpisodeReason = "startup" | "resume" | "network-change" | "connection-loss";
3
+ /** Provenance of a dial; explicit requests and group joins are also distinguished. */
4
+ export type MeshDialReason = "own-device" | "group-network-info" | "broadcast-answer" | "configured-server" | "lan-scan" | "websocket-upgrade" | "incoming" | "direct-request" | "group-join";
5
+ /** Bounded path categories used for lifetime counters; attempts also retain the address. */
6
+ export type MeshPath = "websocket" | "webrtc" | "cloud" | "discovery";
7
+ /** Failure categories, independent of free-form transport error messages. */
8
+ export type MeshFailureReason = "ttl0" | "no-route" | "signaling-timeout" | "socket-error" | "socket-timeout" | "handshake" | "rejected" | "closed" | "superseded" | "unsupported";
9
+ /** Observed stage; timestamps are offsets in milliseconds from the attempt's queued time. */
10
+ export type MeshStage = "queued" | "started" | "authenticated" | "admitted" | "ready" | "firstSync";
11
+ /** Optional observational metadata passed through a dial without affecting its result. */
12
+ export interface IMeshDial {
13
+ attemptId?: string;
14
+ reason?: MeshDialReason;
15
+ targetDeviceId?: string;
16
+ }
17
+ /** One dial or inbound connection. Missing stages have not been observed. */
18
+ export interface IMeshAttempt {
19
+ attemptId: string;
20
+ targetDeviceId?: string;
21
+ reason: MeshDialReason;
22
+ direction: "outgoing" | "incoming";
23
+ path: MeshPath;
24
+ address?: string;
25
+ queuedAt: number;
26
+ stages: Partial<Record<MeshStage, number>>;
27
+ answeredDeviceId?: string;
28
+ expectedIdentity?: boolean;
29
+ answered: boolean;
30
+ lastSignalFailure?: {
31
+ reason: "ttl0" | "no-route";
32
+ detail?: string;
33
+ };
34
+ ready: Record<string, number>;
35
+ firstSync: Record<string, number>;
36
+ outcome: "pending" | "admitted" | "failed" | "unfinished";
37
+ endReason?: "buffer-evicted" | "disposed";
38
+ endedAt?: number;
39
+ durationMs?: number;
40
+ failure?: {
41
+ stage: MeshStage;
42
+ reason: MeshFailureReason;
43
+ detail?: string;
44
+ };
45
+ doubleDial: boolean;
46
+ offline: boolean;
47
+ outOfOrder: boolean;
48
+ }
49
+ /** Millisecond durations derived from one episode; absent means not observed. */
50
+ export interface IMeshDurations {
51
+ T_first?: number;
52
+ T_own?: number;
53
+ T_cover?: number;
54
+ T_resume?: number;
55
+ repairStart?: number;
56
+ }
57
+ /** Coverage at the latest observation (at completion for a finished episode). */
58
+ export interface IMeshContextCoverage {
59
+ dataContextId: string;
60
+ possibleSourceDeviceIds: string[];
61
+ usableDeviceIds: string[];
62
+ excluded: boolean;
63
+ }
64
+ /** One connectivity episode, independent of any other episode in flight. */
65
+ export interface IMeshEpisode {
66
+ episodeId: string;
67
+ reason: MeshEpisodeReason;
68
+ trigger?: string;
69
+ startedAt: number;
70
+ endedAt?: number;
71
+ durationMs?: number;
72
+ outcome: "active" | "covered" | "unfinished";
73
+ endReason?: "covered" | "deadline" | "buffer-evicted" | "disposed";
74
+ metrics: IMeshDurations;
75
+ contexts: IMeshContextCoverage[];
76
+ }
77
+ /** Nearest-rank percentiles of observed values only; count exposes the denominator. */
78
+ export interface IMeshPercentiles {
79
+ count: number;
80
+ p50?: number;
81
+ p90?: number;
82
+ p95?: number;
83
+ p99?: number;
84
+ }
85
+ /** Lifetime dial totals. Incoming connections are separate from the outgoing-dial ratio. */
86
+ export interface IMeshPathCounters {
87
+ dials: number;
88
+ connections: number;
89
+ incomingConnections: number;
90
+ dialsPerConnection: number | null;
91
+ failures: Partial<Record<MeshFailureReason, number>>;
92
+ }
93
+ /** Shared relay totals and current gauges, also exposed on non-Services hosts. */
94
+ export interface IMeshServicesCounters {
95
+ directRelays: number;
96
+ floodsForwarded: number;
97
+ floodsPruned: number;
98
+ pendingEmits: number;
99
+ concurrentConnections: number;
100
+ connects: number;
101
+ disconnects: number;
102
+ }
103
+ /** In-memory diagnostics snapshot. No history survives a runtime restart. */
104
+ export interface IMeshStats {
105
+ startedAt: number;
106
+ sampledAt: number;
107
+ uptimeMs: number;
108
+ readyToDialMs?: number;
109
+ limits: {
110
+ episodes: number;
111
+ attempts: number;
112
+ episodeMs: number;
113
+ };
114
+ currentEpisodes: IMeshEpisode[];
115
+ recentEpisodes: IMeshEpisode[];
116
+ percentiles: Record<keyof IMeshDurations, IMeshPercentiles>;
117
+ counters: {
118
+ paths: Record<MeshPath, IMeshPathCounters>;
119
+ lanScan: {
120
+ scans: number;
121
+ probes: number;
122
+ connected: number;
123
+ };
124
+ unfinishedAttempts: number;
125
+ doubleDials: number;
126
+ offlineDials: number;
127
+ offlineDialsPerHour: number;
128
+ broadcasts: {
129
+ sent: number;
130
+ answered: number;
131
+ ttl0: number;
132
+ failed: number;
133
+ pending: number;
134
+ perHour: number;
135
+ };
136
+ services: IMeshServicesCounters;
137
+ };
138
+ recentAttempts: IMeshAttempt[];
139
+ recentBroadcasts: IMeshBroadcast[];
140
+ }
141
+ /** A discovery broadcast and its observed reply, bounded with the attempt history. */
142
+ export interface IMeshBroadcast {
143
+ broadcastId: string;
144
+ dataContextId?: string;
145
+ sentAt: number;
146
+ durationMs?: number;
147
+ outcome: "pending" | "answered" | "ttl0" | "failed";
148
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -4,12 +4,15 @@ import type { Observable } from "../../observable";
4
4
  import type { INetworkInfo } from "../../types/peer-device";
5
5
  import type { IContractConsumerBase } from "../contract-proxy";
6
6
  import type { IContractDefinition } from "../types";
7
+ import type { IMeshStats } from "./mesh-metrics-types";
8
+ export type * from "./mesh-metrics-types";
7
9
  /** Version of the built-in System Network Diagnostics contract. */
8
10
  export declare const NETWORK_DIAGNOSTICS_CONTRACT_VERSION = 1;
9
11
  /** Version of the built-in System Network Control contract. */
10
12
  export declare const NETWORK_CONTROL_CONTRACT_VERSION = 1;
11
13
  /** Stable IDs for tools in System Network Diagnostics v1. */
12
14
  export declare const networkDiagnosticsToolIds: {
15
+ readonly getMeshStats: string;
13
16
  readonly getOverview: "00ms1yb50ym353ads0gel5jpv";
14
17
  readonly getConnections: "00ms1yb50ykny9kdgiclpa1t4";
15
18
  readonly getGroupSyncStatus: "00ms1yb50yp2q7jr0jglzsztq";
@@ -241,6 +244,11 @@ export interface INetworkDiagnosticsConsumer extends IContractConsumerBase {
241
244
  getOverview(): Promise<{
242
245
  overview: INetworkOverview;
243
246
  }>;
247
+ getMeshStats(input?: {
248
+ limit?: number;
249
+ }): Promise<{
250
+ stats: IMeshStats;
251
+ }>;
244
252
  getConnections(): Promise<{
245
253
  connections: INetworkConnectionInfo[];
246
254
  }>;
@@ -6,12 +6,18 @@ exports.getNetworkControlContractDefinition = getNetworkControlContractDefinitio
6
6
  const system_ids_1 = require("../../system-ids");
7
7
  const field_type_1 = require("../../types/field-type");
8
8
  const system_contracts_1 = require("../system-contracts");
9
+ const tool_catalog_identity_1 = require("../tool-catalog-identity");
9
10
  /** Version of the built-in System Network Diagnostics contract. */
10
11
  exports.NETWORK_DIAGNOSTICS_CONTRACT_VERSION = 1;
11
12
  /** Version of the built-in System Network Control contract. */
12
13
  exports.NETWORK_CONTROL_CONTRACT_VERSION = 1;
13
14
  /** Stable IDs for tools in System Network Diagnostics v1. */
14
15
  exports.networkDiagnosticsToolIds = {
16
+ getMeshStats: (0, tool_catalog_identity_1.deriveToolCatalogId)({
17
+ contractId: system_ids_1.networkDiagnosticsContractId,
18
+ version: exports.NETWORK_DIAGNOSTICS_CONTRACT_VERSION,
19
+ toolName: "getMeshStats",
20
+ }),
15
21
  getOverview: "00ms1yb50ym353ads0gel5jpv",
16
22
  getConnections: "00ms1yb50ykny9kdgiclpa1t4",
17
23
  getGroupSyncStatus: "00ms1yb50yp2q7jr0jglzsztq",
@@ -46,6 +52,16 @@ function getNetworkDiagnosticsContractDefinition() {
46
52
  description: "Portable local network, connection, and synchronization diagnostics.",
47
53
  tables: [],
48
54
  tools: [
55
+ {
56
+ name: "getMeshStats",
57
+ usageDescription: "Read bounded in-memory connectivity episodes, attempts, percentiles and relay counters.",
58
+ inputFields: [
59
+ field("limit", field_type_1.FieldType.number, "Maximum recent attempts and broadcasts (default 50, range 0–500). Episodes and counters are always complete.", { optional: true }),
60
+ ],
61
+ outputFields: [
62
+ field("stats", field_type_1.FieldType.object, "Connectivity measurements since runtime startup."),
63
+ ],
64
+ },
49
65
  {
50
66
  name: "getOverview",
51
67
  usageDescription: "Return aggregate network and group information.",
@@ -44,6 +44,11 @@ export interface ChunkStore {
44
44
  * @returns How many chunks were deleted.
45
45
  */
46
46
  delete(hashes: readonly string[], storedBefore: number): Promise<number>;
47
+ /**
48
+ * Delete on an already-open store without yielding, so collection's liveness check and
49
+ * deletion are one synchronous step. Node stores provide this; IndexedDB cannot.
50
+ */
51
+ deleteSync?(hashes: readonly string[], storedBefore: number): IChunkStoreStats;
47
52
  /**
48
53
  * A page of stored chunks, in hash order.
49
54
  *
@@ -76,6 +81,8 @@ export declare class MemoryChunkStore implements ChunkStore {
76
81
  get(hash: string): Promise<Uint8Array | undefined>;
77
82
  put(hash: string, bytes: Uint8Array): Promise<void>;
78
83
  delete(hashes: readonly string[], storedBefore: number): Promise<number>;
84
+ /** Delete without yielding, using the same age check as the persistent Node store. */
85
+ deleteSync(hashes: readonly string[], storedBefore: number): IChunkStoreStats;
79
86
  list(opts?: {
80
87
  after?: string;
81
88
  limit?: number;
@@ -32,12 +32,17 @@ class MemoryChunkStore {
32
32
  this.chunks.set(hash, { bytes: stored?.bytes ?? new Uint8Array(bytes), storedAt: this.now() });
33
33
  }
34
34
  async delete(hashes, storedBefore) {
35
- let deleted = 0;
35
+ return this.deleteSync(hashes, storedBefore).count;
36
+ }
37
+ /** Delete without yielding, using the same age check as the persistent Node store. */
38
+ deleteSync(hashes, storedBefore) {
39
+ const deleted = { count: 0, bytes: 0 };
36
40
  for (const hash of hashes) {
37
41
  const stored = this.chunks.get(hash);
38
42
  if (stored && stored.storedAt < storedBefore) {
39
43
  this.chunks.delete(hash);
40
- deleted++;
44
+ deleted.count++;
45
+ deleted.bytes += stored.bytes.length;
41
46
  }
42
47
  }
43
48
  return deleted;
@@ -31,6 +31,12 @@ export declare function isLegacyFileId(fileId: string): boolean;
31
31
  * @param file - The row's id and name.
32
32
  */
33
33
  export declare function isLegacyBundleFile(file: Pick<IFile, "fileId" | "name">): boolean;
34
+ /**
35
+ * Whether this is a runtime-owned package bundle: a legacy bundle, or a derived file with
36
+ * an installer bundle name, JavaScript MIME type and keep=false. Read keep through its
37
+ * identity when an older device relayed the row without that column.
38
+ */
39
+ export declare function isRuntimeBundleFile(file: IFile): boolean;
34
40
  /**
35
41
  * Whether a file is kept: never collected, even with nothing referencing it. A legacy row always
36
42
  * is. A row relayed by a build that predates `keep` arrives without it; its id, derived from
@@ -39,6 +45,8 @@ export declare function isLegacyBundleFile(file: Pick<IFile, "fileId" | "name">)
39
45
  * @param file - The row.
40
46
  */
41
47
  export declare function isKeptFile(file: IFile): boolean;
48
+ /** Whether an unreferenced file may expire, including the runtime's bundle exception. */
49
+ export declare function isCollectableFile(file: IFile): boolean;
42
50
  /**
43
51
  * Whether two rows describe the same file, ignoring how an older build stored it (no `keep`, no
44
52
  * MIME type).
@@ -58,7 +66,8 @@ export declare function isSameFile(a: IFile, b: IFile): boolean;
58
66
  export declare function fileRowProblem(file: IFile, recordId: string): string | undefined;
59
67
  /**
60
68
  * The `Files` write check: a new row must pass {@link fileRowProblem}; a stored row never
61
- * changes (rewriting the same file is allowed); and only a legacy bundle row may be deleted.
69
+ * changes (rewriting the same file is allowed); and only a stored runtime bundle row may be
70
+ * deleted. Unknown legacy ids remain allowed.
62
71
  *
63
72
  * @param existing - The row as stored, if any.
64
73
  * @param candidate - The row to store, or undefined for a delete.
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fileIdFor = fileIdFor;
4
4
  exports.isLegacyFileId = isLegacyFileId;
5
5
  exports.isLegacyBundleFile = isLegacyBundleFile;
6
+ exports.isRuntimeBundleFile = isRuntimeBundleFile;
6
7
  exports.isKeptFile = isKeptFile;
8
+ exports.isCollectableFile = isCollectableFile;
7
9
  exports.isSameFile = isSameFile;
8
10
  exports.fileRowProblem = fileRowProblem;
9
11
  exports.checkFileWrite = checkFileWrite;
10
12
  /**
11
13
  * The rules every `Files` row meets, on every device: ids derived from the content, rows that
12
- * never change, and deletes only of runtime-owned legacy bundle rows. Each device checks an
14
+ * never change, and deletes only of runtime bundle rows. Each device checks an
13
15
  * incoming row in full, so no member can put other content under a file's id.
14
16
  */
15
17
  const derived_row_id_1 = require("../derived-row-id");
@@ -57,6 +59,18 @@ function isLegacyFileId(fileId) {
57
59
  function isLegacyBundleFile(file) {
58
60
  return isLegacyFileId(file.fileId) && BUNDLE_FILE_NAME.test(file.name);
59
61
  }
62
+ /**
63
+ * Whether this is a runtime-owned package bundle: a legacy bundle, or a derived file with
64
+ * an installer bundle name, JavaScript MIME type and keep=false. Read keep through its
65
+ * identity when an older device relayed the row without that column.
66
+ */
67
+ function isRuntimeBundleFile(file) {
68
+ return (isLegacyBundleFile(file) ||
69
+ (!isLegacyFileId(file.fileId) &&
70
+ BUNDLE_FILE_NAME.test(file.name) &&
71
+ file.mimeType === "application/javascript" &&
72
+ !isKeptFile(file)));
73
+ }
60
74
  /**
61
75
  * Whether a file is kept: never collected, even with nothing referencing it. A legacy row always
62
76
  * is. A row relayed by a build that predates `keep` arrives without it; its id, derived from
@@ -71,6 +85,10 @@ function isKeptFile(file) {
71
85
  return file.keep;
72
86
  return fileIdFor({ ...file, keep: false }) !== file.fileId;
73
87
  }
88
+ /** Whether an unreferenced file may expire, including the runtime's bundle exception. */
89
+ function isCollectableFile(file) {
90
+ return isRuntimeBundleFile(file) || !isKeptFile(file);
91
+ }
74
92
  /**
75
93
  * Whether two rows describe the same file, ignoring how an older build stored it (no `keep`, no
76
94
  * MIME type).
@@ -133,7 +151,8 @@ function fileRowProblem(file, recordId) {
133
151
  }
134
152
  /**
135
153
  * The `Files` write check: a new row must pass {@link fileRowProblem}; a stored row never
136
- * changes (rewriting the same file is allowed); and only a legacy bundle row may be deleted.
154
+ * changes (rewriting the same file is allowed); and only a stored runtime bundle row may be
155
+ * deleted. Unknown legacy ids remain allowed.
137
156
  *
138
157
  * @param existing - The row as stored, if any.
139
158
  * @param candidate - The row to store, or undefined for a delete.
@@ -142,9 +161,14 @@ function fileRowProblem(file, recordId) {
142
161
  */
143
162
  function checkFileWrite(existing, candidate, recordId) {
144
163
  if (!candidate) {
145
- if (isLegacyFileId(recordId) && (!existing || isLegacyBundleFile(existing)))
164
+ if (existing ? isRuntimeBundleFile(existing) : isLegacyFileId(recordId))
146
165
  return;
147
- throw (0, write_checks_1.writeNotAuthorized)("Files", "a file can't be deleted");
166
+ if (existing)
167
+ throw (0, write_checks_1.writeNotAuthorized)("Files", "a file can't be deleted");
168
+ // An unknown derived id carries no evidence that the row is a bundle. Refuse its delete
169
+ // so a later non-bundle row is still materialized from its authorized history. No later
170
+ // membership data changes that, so sync needn't keep the refusal for recovery.
171
+ throw (0, write_checks_1.writeRefusedForGood)("Files", "a file can't be deleted");
148
172
  }
149
173
  if (existing) {
150
174
  if (!isSameFile(existing, candidate))
@@ -28,7 +28,8 @@ export declare class FileWriteStream {
28
28
  private storeChunk;
29
29
  /**
30
30
  * Store the last chunk and save the file's row. Saving content that already has a row (the
31
- * same bytes, name, MIME type and `keep`) writes no row and returns the stored one.
31
+ * same bytes, name, MIME type and `keep`) returns the stored one; only a package bundle's row
32
+ * is written again, so a peer that deleted it gets it back.
32
33
  *
33
34
  * @returns The file's row.
34
35
  */
@@ -76,7 +76,8 @@ class FileWriteStream {
76
76
  }
77
77
  /**
78
78
  * Store the last chunk and save the file's row. Saving content that already has a row (the
79
- * same bytes, name, MIME type and `keep`) writes no row and returns the stored one.
79
+ * same bytes, name, MIME type and `keep`) returns the stored one; only a package bundle's row
80
+ * is written again, so a peer that deleted it gets it back.
80
81
  *
81
82
  * @returns The file's row.
82
83
  */
@@ -59,6 +59,21 @@ export declare const FILE_MAX_CHUNKS = 1024;
59
59
  export declare const FILE_MAX_SIZE: number;
60
60
  /** Most chunks a read fetches from other devices at once, so a large file isn't one stream. */
61
61
  export declare const FILE_FETCH_WINDOW = 16;
62
+ /** Default time an unreferenced file is retained, before the grace period (30 days). */
63
+ export declare const FILE_RETENTION_MS: number;
64
+ /** Extra time for a reference to reach devices holding the bytes (7 days). */
65
+ export declare const FILE_COLLECTION_GRACE_MS: number;
66
+ /** Minimum age of a stored chunk before collection (one day, longer than a write session). */
67
+ export declare const FILE_CHUNK_FLOOR_MS: number;
68
+ /**
69
+ * Collection delays. `PEERS_FILE_COLLECTION_DELAY_MS` overrides all three for tests only;
70
+ * absent or invalid uses the constants. Explicit group retention is unaffected.
71
+ */
72
+ export declare function fileCollectionDelays(): {
73
+ retentionMs: number;
74
+ graceMs: number;
75
+ chunkFloorMs: number;
76
+ };
62
77
  /**
63
78
  * A file's `fileHash`: the SHA-256 of its chunk hashes as JSON, in order.
64
79
  *
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FILE_FETCH_WINDOW = exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS = exports.FILE_CHUNK_SIZE = exports.filesMetaData = exports.fileSchema = void 0;
3
+ exports.FILE_CHUNK_FLOOR_MS = exports.FILE_COLLECTION_GRACE_MS = exports.FILE_RETENTION_MS = exports.FILE_FETCH_WINDOW = exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS = exports.FILE_CHUNK_SIZE = exports.filesMetaData = exports.fileSchema = void 0;
4
+ exports.fileCollectionDelays = fileCollectionDelays;
4
5
  exports.fileHashOf = fileHashOf;
5
6
  exports.computeFileHash = computeFileHash;
6
7
  const zod_1 = require("zod");
@@ -38,6 +39,26 @@ exports.FILE_MAX_CHUNKS = 1024;
38
39
  exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS * exports.FILE_CHUNK_SIZE;
39
40
  /** Most chunks a read fetches from other devices at once, so a large file isn't one stream. */
40
41
  exports.FILE_FETCH_WINDOW = 16;
42
+ /** Default time an unreferenced file is retained, before the grace period (30 days). */
43
+ exports.FILE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
44
+ /** Extra time for a reference to reach devices holding the bytes (7 days). */
45
+ exports.FILE_COLLECTION_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
46
+ /** Minimum age of a stored chunk before collection (one day, longer than a write session). */
47
+ exports.FILE_CHUNK_FLOOR_MS = 24 * 60 * 60 * 1000;
48
+ /**
49
+ * Collection delays. `PEERS_FILE_COLLECTION_DELAY_MS` overrides all three for tests only;
50
+ * absent or invalid uses the constants. Explicit group retention is unaffected.
51
+ */
52
+ function fileCollectionDelays() {
53
+ const value = typeof process === "undefined" ? undefined : process.env.PEERS_FILE_COLLECTION_DELAY_MS;
54
+ const override = value === undefined || value.trim() === "" ? NaN : Number(value);
55
+ const delay = (fallback) => Number.isFinite(override) && override >= 0 ? override : fallback;
56
+ return {
57
+ retentionMs: delay(exports.FILE_RETENTION_MS),
58
+ graceMs: delay(exports.FILE_COLLECTION_GRACE_MS),
59
+ chunkFloorMs: delay(exports.FILE_CHUNK_FLOOR_MS),
60
+ };
61
+ }
41
62
  /**
42
63
  * A file's `fileHash`: the SHA-256 of its chunk hashes as JSON, in order.
43
64
  *
@@ -28,8 +28,9 @@ export declare class FilesTable extends Table<IFile> {
28
28
  /**
29
29
  * Store a file's row as it is, after the checks every device makes: a new row must have the
30
30
  * id derived from its content (or be a legacy row), and a stored row never changes. Storing
31
- * the same file again writes nothing. Writers use {@link saveFile} or a write stream; this is
32
- * for copying a row from another context with its id.
31
+ * an existing ordinary file again writes nothing; saving a runtime bundle publishes a newer
32
+ * synced snapshot, including when restoring a deleted row. Writers use {@link saveFile} or a
33
+ * write stream; this is for copying a row from another context with its id.
33
34
  *
34
35
  * @param fileRecord - The row.
35
36
  * @returns The row as stored.
@@ -73,11 +74,11 @@ export declare class FilesTable extends Table<IFile> {
73
74
  */
74
75
  fetchFile(fileId: string): Promise<boolean>;
75
76
  /**
76
- * Delete a legacy bundle row, which the runtime owns. Every other file is permanent: it's
77
+ * Delete a runtime bundle row, which the runtime owns. Every other file is permanent: it's
77
78
  * collected once nothing references it. Chunks stay stored, since other files may share them.
78
79
  *
79
80
  * @param fileId - File to delete. A missing file is not an error.
80
- * @throws When the file isn't a legacy bundle row.
81
+ * @throws When the file isn't a runtime bundle row.
81
82
  */
82
83
  deleteFile(fileId: string): Promise<void>;
83
84
  }
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FilesTable = void 0;
4
4
  exports.Files = Files;
5
5
  const user_context_singleton_1 = require("../../context/user-context-singleton");
6
- const rpc_types_1 = require("../../rpc-types");
7
6
  const orm_1 = require("../orm");
8
7
  const table_definitions_system_1 = require("../orm/table-definitions.system");
9
8
  const chunk_store_1 = require("./chunk-store");
@@ -46,8 +45,9 @@ class FilesTable extends orm_1.Table {
46
45
  /**
47
46
  * Store a file's row as it is, after the checks every device makes: a new row must have the
48
47
  * id derived from its content (or be a legacy row), and a stored row never changes. Storing
49
- * the same file again writes nothing. Writers use {@link saveFile} or a write stream; this is
50
- * for copying a row from another context with its id.
48
+ * an existing ordinary file again writes nothing; saving a runtime bundle publishes a newer
49
+ * synced snapshot, including when restoring a deleted row. Writers use {@link saveFile} or a
50
+ * write stream; this is for copying a row from another context with its id.
51
51
  *
52
52
  * @param fileRecord - The row.
53
53
  * @returns The row as stored.
@@ -56,11 +56,18 @@ class FilesTable extends orm_1.Table {
56
56
  async saveFileRecord(fileRecord) {
57
57
  const existing = await super.get(fileRecord.fileId);
58
58
  (0, file_rows_1.checkFileWrite)(existing, fileRecord, fileRecord.fileId);
59
- if (existing)
59
+ const runtimeBundle = (0, file_rows_1.isRuntimeBundleFile)(fileRecord);
60
+ if (existing && !runtimeBundle)
60
61
  return existing;
61
- // Another save or sync apply may have inserted the row since the read. In particular, the
62
- // personal context has no data-source write check, so never let this become an update.
63
- const saved = await super.save(fileRecord, { insertOnly: true });
62
+ // An ordinary file inserted since the read must stay untouched. Bundles can be rewritten
63
+ // identically so their pending reuse reaches collection's write watch and other devices.
64
+ const saved = await super.save(existing ?? fileRecord, {
65
+ insertOnly: !runtimeBundle || !existing,
66
+ // Even when still present locally, a reused bundle needs a new snapshot: another device
67
+ // may have deleted it, and collection's write watch must protect this pending reuse.
68
+ restoreIfDeleted: runtimeBundle,
69
+ saveAsSnapshot: runtimeBundle,
70
+ });
64
71
  (0, file_rows_1.checkFileWrite)(saved, fileRecord, fileRecord.fileId);
65
72
  return saved;
66
73
  }
@@ -132,11 +139,11 @@ class FilesTable extends orm_1.Table {
132
139
  return complete;
133
140
  }
134
141
  /**
135
- * Delete a legacy bundle row, which the runtime owns. Every other file is permanent: it's
142
+ * Delete a runtime bundle row, which the runtime owns. Every other file is permanent: it's
136
143
  * collected once nothing references it. Chunks stay stored, since other files may share them.
137
144
  *
138
145
  * @param fileId - File to delete. A missing file is not an error.
139
- * @throws When the file isn't a legacy bundle row.
146
+ * @throws When the file isn't a runtime bundle row.
140
147
  */
141
148
  async deleteFile(fileId) {
142
149
  const fileRecord = await super.get(fileId);
@@ -158,18 +165,3 @@ exports.FilesTable = FilesTable;
158
165
  function Files(dataContext) {
159
166
  return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(file_types_1.filesMetaData, file_types_1.fileSchema);
160
167
  }
161
- // Deprecated RPCs, kept only for Family Hub 0.3.x until its port is Home's version. New code
162
- // uses System Files, which is bound to the caller's own data context.
163
- rpc_types_1.rpcServerCalls.getFileContentsBase64 = async (fileId) => {
164
- const data = await Files().getFileContents(fileId);
165
- if (data === null) {
166
- throw new Error(`File not found: ${fileId}`);
167
- }
168
- return Buffer.from(data).toString("base64");
169
- };
170
- rpc_types_1.rpcServerCalls.saveFile = async (input) => {
171
- const encoding = input.encoding || "utf8";
172
- const dataBuffer = encoding === "base64" ? Buffer.from(input.data, "base64") : Buffer.from(input.data, "utf8");
173
- // Kept: Family Hub 0.3.x doesn't declare its file fields, so nothing would reference these.
174
- return await Files().saveFile({ name: input.name, mimeType: input.mimeType, keep: true }, new Uint8Array(dataBuffer));
175
- };
@@ -12,6 +12,13 @@ export declare const groupSchema: z.ZodObject<{
12
12
  iconClassName: z.ZodOptional<z.ZodString>;
13
13
  publicRole: z.ZodOptional<z.ZodNativeEnum<typeof GroupMemberRole>>;
14
14
  disabled: z.ZodOptional<z.ZodBoolean>;
15
+ fileRetention: z.ZodOptional<z.ZodObject<{
16
+ days: z.ZodUnion<[z.ZodNumber, z.ZodLiteral<"never">]>;
17
+ }, "strip", z.ZodTypeAny, {
18
+ days: number | "never";
19
+ }, {
20
+ days: number | "never";
21
+ }>>;
15
22
  founderUserId: z.ZodEffects<z.ZodString, string, string>;
16
23
  signature: z.ZodString;
17
24
  signedAt: z.ZodOptional<z.ZodNumber>;
@@ -29,6 +36,9 @@ export declare const groupSchema: z.ZodObject<{
29
36
  iconClassName?: string | undefined;
30
37
  signedAt?: number | undefined;
31
38
  publicRole?: GroupMemberRole | undefined;
39
+ fileRetention?: {
40
+ days: number | "never";
41
+ } | undefined;
32
42
  }, {
33
43
  name: string;
34
44
  description: string;
@@ -41,6 +51,9 @@ export declare const groupSchema: z.ZodObject<{
41
51
  iconClassName?: string | undefined;
42
52
  signedAt?: number | undefined;
43
53
  publicRole?: GroupMemberRole | undefined;
54
+ fileRetention?: {
55
+ days: number | "never";
56
+ } | undefined;
44
57
  }>;
45
58
  /** A signed group record shared between personal and group contexts. */
46
59
  export type IGroup = z.infer<typeof groupSchema>;
@@ -60,6 +60,12 @@ exports.groupSchema = zod_1.z.object({
60
60
  .optional()
61
61
  .describe("Whether the group is publicly readable"),
62
62
  disabled: zod_1.z.boolean().optional().describe("Whether the group is currently disabled"),
63
+ // An object column stores JSON, so the number reads back as signed; a union would be a TEXT
64
+ // column that returns "30.0", and the row's signature would then fail.
65
+ fileRetention: zod_1.z
66
+ .object({ days: zod_1.z.union([zod_1.z.number().int().nonnegative(), zod_1.z.literal("never")]) })
67
+ .optional()
68
+ .describe("Days to retain unreferenced files before the grace period, or never; absent means 30 days."),
63
69
  founderUserId: zod_types_1.zodPeerId.describe("The user ID of the group founder"),
64
70
  signature: zod_1.z.string().describe("The signed hash of this data excluding the signature itself"),
65
71
  signedAt: zod_1.z
@@ -89,3 +89,18 @@ export declare function deletedSignedAt(tableName: string, context: ITableWriteC
89
89
  * @param reason - Why, for logs and the sync recovery queue.
90
90
  */
91
91
  export declare function writeNotAuthorized(tableName: string, reason: string): Error;
92
+ /**
93
+ * A {@link writeNotAuthorized} refusal that no data arriving later can lift, unlike one that
94
+ * waits on Users, Groups or GroupMembers rows still to sync. Sync keeps no recovery entry for
95
+ * such a refused delete of a record it has no row or other history for.
96
+ *
97
+ * @param tableName - Table the write was for.
98
+ * @param reason - Why, for logs.
99
+ */
100
+ export declare function writeRefusedForGood(tableName: string, reason: string): Error;
101
+ /**
102
+ * Whether an error is a {@link writeRefusedForGood} refusal.
103
+ *
104
+ * @param error - The error a write check threw.
105
+ */
106
+ export declare function isWriteRefusedForGood(error: unknown): boolean;
@@ -7,6 +7,8 @@ exports.checkLocalSave = checkLocalSave;
7
7
  exports.signingStamp = signingStamp;
8
8
  exports.deletedSignedAt = deletedSignedAt;
9
9
  exports.writeNotAuthorized = writeNotAuthorized;
10
+ exports.writeRefusedForGood = writeRefusedForGood;
11
+ exports.isWriteRefusedForGood = isWriteRefusedForGood;
10
12
  /**
11
13
  * Prefix of every refusal from a {@link TableWriteCheck}. Sync recovery parks a change refused
12
14
  * with it, and checks it again only from local history, when the group's authority changes.
@@ -120,3 +122,22 @@ async function deletedSignedAt(tableName, context, existing) {
120
122
  function writeNotAuthorized(tableName, reason) {
121
123
  return new Error(`${exports.WRITE_NOT_AUTHORIZED}: ${tableName}: ${reason}`);
122
124
  }
125
+ /**
126
+ * A {@link writeNotAuthorized} refusal that no data arriving later can lift, unlike one that
127
+ * waits on Users, Groups or GroupMembers rows still to sync. Sync keeps no recovery entry for
128
+ * such a refused delete of a record it has no row or other history for.
129
+ *
130
+ * @param tableName - Table the write was for.
131
+ * @param reason - Why, for logs.
132
+ */
133
+ function writeRefusedForGood(tableName, reason) {
134
+ return Object.assign(writeNotAuthorized(tableName, reason), { refusedForGood: true });
135
+ }
136
+ /**
137
+ * Whether an error is a {@link writeRefusedForGood} refusal.
138
+ *
139
+ * @param error - The error a write check threw.
140
+ */
141
+ function isWriteRefusedForGood(error) {
142
+ return error?.refusedForGood === true;
143
+ }
@@ -21,6 +21,9 @@ export declare class Connection {
21
21
  private _remoteDeviceInfo;
22
22
  /** Trust assigned to the remote identity once the handshake completes. */
23
23
  trustLevel: TrustLevel;
24
+ /** Synchronous diagnostics only; observer errors cannot affect handshake results. */
25
+ onHandshakeObserved?: (stage: "authenticated" | "failed", error?: unknown) => void;
26
+ private observeHandshake;
24
27
  /** Server-side hook fired shortly after the successful handshake reply has been sent. */
25
28
  onHandshakeComplete: ((deviceInfo: IDeviceInfo) => void) | undefined;
26
29
  /** RPC timeout for {@link Connection.emit}. */
@@ -32,6 +32,16 @@ class Connection {
32
32
  };
33
33
  /** Trust assigned to the remote identity once the handshake completes. */
34
34
  trustLevel = socket_type_1.TrustLevel.Unknown;
35
+ /** Synchronous diagnostics only; observer errors cannot affect handshake results. */
36
+ onHandshakeObserved;
37
+ observeHandshake(stage, error) {
38
+ try {
39
+ this.onHandshakeObserved?.(stage, error);
40
+ }
41
+ catch {
42
+ /* diagnostics never affect the handshake */
43
+ }
44
+ }
35
45
  /** Server-side hook fired shortly after the successful handshake reply has been sent. */
36
46
  onHandshakeComplete;
37
47
  /** RPC timeout for {@link Connection.emit}. */
@@ -133,6 +143,7 @@ class Connection {
133
143
  }
134
144
  }
135
145
  catch (e) {
146
+ this.observeHandshake("failed", e);
136
147
  console.error("Error handling completeHandshake", e);
137
148
  callback({ error: e?.message ?? String(e), errorType: "RPC_ERROR" });
138
149
  }
@@ -412,6 +423,7 @@ class Connection {
412
423
  };
413
424
  this.trustLevel = trustLevel;
414
425
  this._verified = true;
426
+ this.observeHandshake("authenticated");
415
427
  return handshakeResponse;
416
428
  }
417
429
  /**
@@ -459,6 +471,7 @@ class Connection {
459
471
  return handshakeResponse;
460
472
  }
461
473
  catch (e) {
474
+ this.observeHandshake("failed", e);
462
475
  // A failed or timed-out attempt must neither retain its ephemeral secret nor let a
463
476
  // late reply authenticate it. Do not clear state belonging to a newer handshake.
464
477
  if (generation === this.handshakeGeneration)
@@ -522,6 +535,7 @@ class Connection {
522
535
  : remoteDeviceInfo.deviceName,
523
536
  };
524
537
  this._verified = true;
538
+ this.observeHandshake("authenticated");
525
539
  this._connectionAddress = remoteAddress;
526
540
  return handshakeResponse;
527
541
  }
@@ -344,6 +344,13 @@ export declare const groupApprovalSchema: z.ZodObject<{
344
344
  iconClassName: z.ZodOptional<z.ZodString>;
345
345
  publicRole: z.ZodOptional<z.ZodNativeEnum<typeof import("../data").GroupMemberRole>>;
346
346
  disabled: z.ZodOptional<z.ZodBoolean>;
347
+ fileRetention: z.ZodOptional<z.ZodObject<{
348
+ days: z.ZodUnion<[z.ZodNumber, z.ZodLiteral<"never">]>;
349
+ }, "strip", z.ZodTypeAny, {
350
+ days: number | "never";
351
+ }, {
352
+ days: number | "never";
353
+ }>>;
347
354
  founderUserId: z.ZodEffects<z.ZodString, string, string>;
348
355
  signature: z.ZodString;
349
356
  signedAt: z.ZodOptional<z.ZodNumber>;
@@ -361,6 +368,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
361
368
  iconClassName?: string | undefined;
362
369
  signedAt?: number | undefined;
363
370
  publicRole?: import("../data").GroupMemberRole | undefined;
371
+ fileRetention?: {
372
+ days: number | "never";
373
+ } | undefined;
364
374
  }, {
365
375
  name: string;
366
376
  description: string;
@@ -373,6 +383,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
373
383
  iconClassName?: string | undefined;
374
384
  signedAt?: number | undefined;
375
385
  publicRole?: import("../data").GroupMemberRole | undefined;
386
+ fileRetention?: {
387
+ days: number | "never";
388
+ } | undefined;
376
389
  }>>;
377
390
  membership: z.ZodLazy<z.ZodObject<{
378
391
  groupMemberId: z.ZodEffects<z.ZodString, string, string>;
@@ -654,6 +667,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
654
667
  iconClassName?: string | undefined;
655
668
  signedAt?: number | undefined;
656
669
  publicRole?: import("../data").GroupMemberRole | undefined;
670
+ fileRetention?: {
671
+ days: number | "never";
672
+ } | undefined;
657
673
  };
658
674
  seed: {
659
675
  users: {
@@ -734,6 +750,9 @@ export declare const groupApprovalSchema: z.ZodObject<{
734
750
  iconClassName?: string | undefined;
735
751
  signedAt?: number | undefined;
736
752
  publicRole?: import("../data").GroupMemberRole | undefined;
753
+ fileRetention?: {
754
+ days: number | "never";
755
+ } | undefined;
737
756
  };
738
757
  seed: {
739
758
  users: {
@@ -1296,6 +1315,13 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
1296
1315
  iconClassName: z.ZodOptional<z.ZodString>;
1297
1316
  publicRole: z.ZodOptional<z.ZodNativeEnum<typeof import("../data").GroupMemberRole>>;
1298
1317
  disabled: z.ZodOptional<z.ZodBoolean>;
1318
+ fileRetention: z.ZodOptional<z.ZodObject<{
1319
+ days: z.ZodUnion<[z.ZodNumber, z.ZodLiteral<"never">]>;
1320
+ }, "strip", z.ZodTypeAny, {
1321
+ days: number | "never";
1322
+ }, {
1323
+ days: number | "never";
1324
+ }>>;
1299
1325
  founderUserId: z.ZodEffects<z.ZodString, string, string>;
1300
1326
  signature: z.ZodString;
1301
1327
  signedAt: z.ZodOptional<z.ZodNumber>;
@@ -1313,6 +1339,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
1313
1339
  iconClassName?: string | undefined;
1314
1340
  signedAt?: number | undefined;
1315
1341
  publicRole?: import("../data").GroupMemberRole | undefined;
1342
+ fileRetention?: {
1343
+ days: number | "never";
1344
+ } | undefined;
1316
1345
  }, {
1317
1346
  name: string;
1318
1347
  description: string;
@@ -1325,6 +1354,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
1325
1354
  iconClassName?: string | undefined;
1326
1355
  signedAt?: number | undefined;
1327
1356
  publicRole?: import("../data").GroupMemberRole | undefined;
1357
+ fileRetention?: {
1358
+ days: number | "never";
1359
+ } | undefined;
1328
1360
  }>>;
1329
1361
  membership: z.ZodLazy<z.ZodObject<{
1330
1362
  groupMemberId: z.ZodEffects<z.ZodString, string, string>;
@@ -1606,6 +1638,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
1606
1638
  iconClassName?: string | undefined;
1607
1639
  signedAt?: number | undefined;
1608
1640
  publicRole?: import("../data").GroupMemberRole | undefined;
1641
+ fileRetention?: {
1642
+ days: number | "never";
1643
+ } | undefined;
1609
1644
  };
1610
1645
  seed: {
1611
1646
  users: {
@@ -1686,6 +1721,9 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
1686
1721
  iconClassName?: string | undefined;
1687
1722
  signedAt?: number | undefined;
1688
1723
  publicRole?: import("../data").GroupMemberRole | undefined;
1724
+ fileRetention?: {
1725
+ days: number | "never";
1726
+ } | undefined;
1689
1727
  };
1690
1728
  seed: {
1691
1729
  users: {
@@ -29,28 +29,6 @@ export interface IRemoteLogsQueryInput {
29
29
  export declare const rpcServerCalls: {
30
30
  encryptData: (value: string, groupId?: string) => Promise<string>;
31
31
  tableMethodCall: (dataContextId: string, tableName: string, methodName: string, ...args: any[]) => Promise<any>;
32
- /**
33
- * @deprecated Use System Files (`SystemFiles`, `readWholeFile`). Kept only for Family Hub
34
- * 0.3.x; deleted once its port is Home's version.
35
- */
36
- getFileContentsBase64: (fileId: string) => Promise<string>;
37
- /**
38
- * @deprecated Use System Files (`SystemFiles`). Kept only for Family Hub 0.3.x; deleted once
39
- * its port is Home's version.
40
- */
41
- saveFile: (input: {
42
- name: string;
43
- mimeType?: string;
44
- data: string;
45
- encoding?: "utf8" | "base64";
46
- fileId?: string;
47
- }) => Promise<{
48
- fileId: string;
49
- name: string;
50
- fileSize: number;
51
- fileHash: string;
52
- mimeType?: string;
53
- }>;
54
32
  deleteLocalDatabase: () => Promise<void>;
55
33
  /** Returns info about the pre-migration data directory, or `null` if already cleaned up. */
56
34
  getOldDataDirInfo: () => Promise<{
package/dist/rpc-types.js CHANGED
@@ -14,16 +14,6 @@ function rpcStub(rpcName) {
14
14
  exports.rpcServerCalls = {
15
15
  encryptData: rpcStub("encryptData"),
16
16
  tableMethodCall: rpcStub("tableMethodCall"),
17
- /**
18
- * @deprecated Use System Files (`SystemFiles`, `readWholeFile`). Kept only for Family Hub
19
- * 0.3.x; deleted once its port is Home's version.
20
- */
21
- getFileContentsBase64: rpcStub("getFileContentsBase64"),
22
- /**
23
- * @deprecated Use System Files (`SystemFiles`). Kept only for Family Hub 0.3.x; deleted once
24
- * its port is Home's version.
25
- */
26
- saveFile: rpcStub("saveFile"),
27
17
  deleteLocalDatabase: rpcStub("deleteLocalDatabase"),
28
18
  /** Returns info about the pre-migration data directory, or `null` if already cleaned up. */
29
19
  getOldDataDirInfo: rpcStub("getOldDataDirInfo"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.30.1",
3
+ "version": "0.30.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"