@peers-app/peers-sdk 0.28.2 → 0.28.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.
@@ -9,6 +9,7 @@ exports.getTableContainer = getTableContainer;
9
9
  exports.setDefaultClientUserContext = setDefaultClientUserContext;
10
10
  exports.getAllTables = getAllTables;
11
11
  const client_proxy_data_source_1 = require("../data/orm/client-proxy.data-source");
12
+ const decorators_1 = require("../data/orm/decorators");
12
13
  const rpc_types_1 = require("../rpc-types");
13
14
  const utils_1 = require("../utils");
14
15
  const user_context_1 = require("./user-context");
@@ -170,6 +171,7 @@ if (!rpc_types_1.isClient) {
170
171
  if (typeof method !== "function") {
171
172
  throw new Error(`Method ${methodName} not found on table ${tableName}`);
172
173
  }
174
+ (0, decorators_1.checkRemoteTableSaveOptions)(methodName, args);
173
175
  // @ts-expect-error - call this way to ensure 'this' is correct
174
176
  return table[methodName](...args);
175
177
  };
@@ -584,6 +584,7 @@ function createContractProvider(resolution, opts = {}) {
584
584
  !(0, decorators_1.isContractRemoteTableMethod)(method)) {
585
585
  throw new Error(`Table method '${call.name}.${call.operation}' is not remotely callable`);
586
586
  }
587
+ (0, decorators_1.checkRemoteTableSaveOptions)(call.operation, call.args);
587
588
  return impl.apply(table, call.args);
588
589
  }
589
590
  case "tool": {
@@ -131,9 +131,10 @@ let GroupMembersTable = (() => {
131
131
  ...(await (0, write_checks_1.signingStamp)(this, groupMember, current)),
132
132
  });
133
133
  return this.save(groupMember, {
134
- saveAsSnapshot: true,
135
134
  restoreIfDeleted: true,
136
135
  ...(opts || {}),
136
+ saveAsSnapshot: true,
137
+ writtenAt: groupMember.signedAt,
137
138
  });
138
139
  }
139
140
  async delete(groupMemberId) {
@@ -39,7 +39,10 @@ async function resignGroupAuthority(dataContext, opts = {}) {
39
39
  try {
40
40
  if (opts.secretKey) {
41
41
  const stamp = await (0, write_checks_1.signingStamp)(table, row, row);
42
- await table.save((0, keys_1.addSignatureToObject)({ ...row, ...stamp }, opts.secretKey), snapshot);
42
+ await table.save((0, keys_1.addSignatureToObject)({ ...row, ...stamp }, opts.secretKey), {
43
+ ...snapshot,
44
+ writtenAt: stamp.signedAt,
45
+ });
43
46
  }
44
47
  else {
45
48
  await table.signAndSave(prepare(row), snapshot);
@@ -116,7 +116,8 @@ let GroupsTable = (() => {
116
116
  ...group,
117
117
  ...(await (0, write_checks_1.signingStamp)(this, group)),
118
118
  });
119
- return this.save(group, opts);
119
+ // Field patches from concurrent writes could combine into a row nobody signed.
120
+ return this.save(group, { ...opts, saveAsSnapshot: true, writtenAt: group.signedAt });
120
121
  }
121
122
  /**
122
123
  * Re-sign this group's authority rows (group row, memberships, packages, package versions)
@@ -1,6 +1,7 @@
1
1
  import type { z } from "zod";
2
2
  import { type DataFilter, type ICursorIterable, type IDataQueryParams, type IDataSource, type ISaveOptions } from "./data-query";
3
3
  import { type ITableMetaData } from "./types";
4
+ /** A renderer's table data source, forwarding operations to the host over RPC. */
4
5
  export declare class ClientProxyDataSource<T extends {
5
6
  [key: string]: any;
6
7
  }> implements IDataSource<T> {
@@ -4,7 +4,9 @@ exports.ClientProxyDataSource = void 0;
4
4
  const rpc_types_1 = require("../../rpc-types");
5
5
  const utils_1 = require("../../utils");
6
6
  const data_query_1 = require("./data-query");
7
+ const decorators_1 = require("./decorators");
7
8
  const types_1 = require("./types");
9
+ /** A renderer's table data source, forwarding operations to the host over RPC. */
8
10
  class ClientProxyDataSource {
9
11
  metaData;
10
12
  schema;
@@ -38,6 +40,7 @@ class ClientProxyDataSource {
38
40
  return rpc_types_1.rpcServerCalls.tableMethodCall(this.dataContextId, this.tableName, "findOne", filter, opts);
39
41
  }
40
42
  async save(record, opts) {
43
+ (0, decorators_1.checkRemoteTableSaveOptions)("save", [record, opts]);
41
44
  return rpc_types_1.rpcServerCalls.tableMethodCall(this.dataContextId, this.tableName, "save", record, opts);
42
45
  }
43
46
  async insert(record) {
@@ -1,9 +1,13 @@
1
+ /** Options controlling how a host table save is recorded. */
1
2
  export interface ISaveOptions {
2
3
  restoreIfDeleted?: boolean;
4
+ /** @internal Host-only change time for signed rows; must match their signedAt. */
5
+ writtenAt?: number;
3
6
  saveAsSnapshot?: boolean;
4
7
  weakInsert?: boolean;
5
8
  insertOnly?: boolean;
6
9
  }
10
+ /** Persistence operations for one table. */
7
11
  export interface IDataSource<T extends {
8
12
  [key: string]: any;
9
13
  }> {
@@ -18,6 +22,7 @@ export interface IDataSource<T extends {
18
22
  update: (record: T) => Promise<T>;
19
23
  delete: (data: string | T) => Promise<void>;
20
24
  }
25
+ /** Table operations needed to execute a data query. */
21
26
  export interface ITable<T extends Record<string, any>> {
22
27
  tableName: string;
23
28
  metaData: {
@@ -34,6 +39,7 @@ export interface ITable<T extends Record<string, any>> {
34
39
  textSearch?: string;
35
40
  }) => Promise<T[]>;
36
41
  }
42
+ /** Filtering, ordering and pagination for table reads. */
37
43
  export interface IDataQueryParams<T extends Record<string, any>> {
38
44
  page?: number;
39
45
  pageSize?: number;
@@ -43,6 +49,7 @@ export interface IDataQueryParams<T extends Record<string, any>> {
43
49
  textSearchExact?: boolean;
44
50
  skipPast?: T;
45
51
  }
52
+ /** A table query that can be listed or consumed through a cursor. */
46
53
  export declare class DataQuery<T extends Record<string, any>> {
47
54
  readonly table: ITable<T>;
48
55
  constructor(table: ITable<T>, { page, pageSize, sortBy, filter, textSearch, textSearchExact }?: IDataQueryParams<T>);
@@ -55,18 +62,23 @@ export declare class DataQuery<T extends Record<string, any>> {
55
62
  cursor(): ICursorIterable<T>;
56
63
  list(): Promise<T[]>;
57
64
  }
65
+ /** A cursor positioned at a row, with optional seek support. */
58
66
  export interface ICursor<T> {
59
67
  value: T | null;
60
68
  next: (skipPast?: T) => Promise<T | null>;
61
69
  }
70
+ /** A cursor that also supports asynchronous iteration. */
62
71
  export type ICursorIterable<T> = ICursor<T> & AsyncIterable<T>;
72
+ /** Adds asynchronous iteration to a cursor. */
63
73
  export declare function iterableCursor<T>(cursor: ICursor<T>): ICursorIterable<T>;
74
+ /** Reads a data source in pages through an asynchronous cursor. */
64
75
  export declare function dataSourceCursor<T extends {
65
76
  [key: string]: any;
66
77
  }>(dataSource: {
67
78
  list: (filter?: DataFilter<T>, opts?: IDataQueryParams<T>) => Promise<T[]>;
68
79
  primaryKeyName: string;
69
80
  }, filter?: DataFilter<T>, opts?: IDataQueryParams<T>): ICursorIterable<T>;
81
+ /** Counts matching rows, falling back to a cursor when native count is unavailable. */
70
82
  export declare function dataSourceCount<T extends {
71
83
  [key: string]: any;
72
84
  }>(dataSource: {
@@ -74,11 +86,15 @@ export declare function dataSourceCount<T extends {
74
86
  list: (filter?: DataFilter<T>, opts?: IDataQueryParams<T>) => Promise<T[]>;
75
87
  primaryKeyName: string;
76
88
  }, filter?: DataFilter<T>): Promise<number>;
89
+ /** Ordered fields; a leading minus selects descending order. */
77
90
  export type SortBy<T extends {
78
91
  [key: string]: any;
79
92
  }> = (keyof T extends string | number ? keyof T | `-${keyof T}` : never)[];
93
+ /** Scalar values accepted by data filters. */
80
94
  export type DataFieldScalar = boolean | number | string | Date | null;
95
+ /** Comparison operators accepted in a field filter. */
81
96
  export type DataFilterValueOperator = "$eq" | "$ne" | "$gt" | "$gte" | "$lt" | "$lte" | "$exists" | "$matchWords" | "$substr" | "$expr" | "$in";
97
+ /** A field value or comparison used in a data filter. */
82
98
  export type DataFilterValue = DataFieldScalar | DataFieldScalar[] | {
83
99
  [key in Exclude<DataFilterValueOperator, "$in">]?: DataFieldScalar;
84
100
  } | {
@@ -86,13 +102,17 @@ export type DataFilterValue = DataFieldScalar | DataFieldScalar[] | {
86
102
  } | {
87
103
  $in: DataFieldScalar[];
88
104
  };
105
+ /** Field predicates combined with AND. */
89
106
  export type DataFilterAnd<T> = {
90
107
  [key in keyof T]?: DataFilterValue;
91
108
  };
109
+ /** Alternative groups of field predicates combined with OR. */
92
110
  export type DataFilterOr<T> = DataFilterAnd<T>[] | {
93
111
  $or: DataFilterAnd<T>[];
94
112
  };
113
+ /** A table filter composed of field predicates and logical groups. */
95
114
  export type DataFilter<T> = DataFilterAnd<T> | DataFilterOr<T> | {
96
115
  $and: DataFilter<T>[];
97
116
  };
117
+ /** Adds predicates to resume an ordered query after the supplied row. */
98
118
  export declare function filterToSkipPastValue(filter: DataFilter<unknown>, sortBy: string[], skipPast: any): DataFilter<unknown>;
@@ -5,6 +5,7 @@ exports.iterableCursor = iterableCursor;
5
5
  exports.dataSourceCursor = dataSourceCursor;
6
6
  exports.dataSourceCount = dataSourceCount;
7
7
  exports.filterToSkipPastValue = filterToSkipPastValue;
8
+ /** A table query that can be listed or consumed through a cursor. */
8
9
  class DataQuery {
9
10
  table;
10
11
  constructor(table, { page, pageSize, sortBy, filter, textSearch, textSearchExact } = {}) {
@@ -85,6 +86,7 @@ class DataQuery {
85
86
  }
86
87
  }
87
88
  exports.DataQuery = DataQuery;
89
+ /** Adds asynchronous iteration to a cursor. */
88
90
  function iterableCursor(cursor) {
89
91
  // @ts-expect-error - we're explicitly extending the cursor object to have this symbol
90
92
  cursor[Symbol.asyncIterator] = () => {
@@ -97,6 +99,7 @@ function iterableCursor(cursor) {
97
99
  };
98
100
  return cursor;
99
101
  }
102
+ /** Reads a data source in pages through an asynchronous cursor. */
100
103
  function dataSourceCursor(dataSource, filter = {}, opts = {}) {
101
104
  // if (dataSource.cursor) {
102
105
  // return dataSource.cursor(filter, opts);
@@ -157,6 +160,7 @@ function dataSourceCursor(dataSource, filter = {}, opts = {}) {
157
160
  };
158
161
  return iterableCursor(cursor);
159
162
  }
163
+ /** Counts matching rows, falling back to a cursor when native count is unavailable. */
160
164
  async function dataSourceCount(dataSource, filter = {}) {
161
165
  if (dataSource.count) {
162
166
  return dataSource.count(filter);
@@ -169,6 +173,7 @@ async function dataSourceCount(dataSource, filter = {}) {
169
173
  }
170
174
  return count;
171
175
  }
176
+ /** Adds predicates to resume an ordered query after the supplied row. */
172
177
  function filterToSkipPastValue(filter, sortBy, skipPast) {
173
178
  const sortByFilters = [];
174
179
  for (let i = 0; i < sortBy.length; i++) {
@@ -1,3 +1,5 @@
1
+ /** Refuse host-only signed write times supplied through a table RPC or contract call. */
2
+ export declare function checkRemoteTableSaveOptions(methodName: string, args: readonly unknown[]): void;
1
3
  /**
2
4
  * Return whether a table method was explicitly marked for remote invocation by
3
5
  * {@link ProxyClientTableMethodCalls}. Client proxies are recognized through their
@@ -1,9 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkRemoteTableSaveOptions = checkRemoteTableSaveOptions;
3
4
  exports.isContractRemoteTableMethod = isContractRemoteTableMethod;
4
5
  exports.ProxyClientTableMethodCalls = ProxyClientTableMethodCalls;
5
6
  const rpc_types_1 = require("../../rpc-types");
6
7
  const contractRemoteTableMethod = Symbol("contractRemoteTableMethod");
8
+ /** Refuse host-only signed write times supplied through a table RPC or contract call. */
9
+ function checkRemoteTableSaveOptions(methodName, args) {
10
+ if (!["save", "insert", "update", "signAndSave"].includes(methodName))
11
+ return;
12
+ const options = args[1];
13
+ if (options && typeof options === "object" && "writtenAt" in options) {
14
+ throw new Error("writtenAt is host-only; use signAndSave to sign a new change");
15
+ }
16
+ }
7
17
  function markContractRemoteTableMethod(method) {
8
18
  Object.defineProperty(method, contractRemoteTableMethod, { value: true });
9
19
  }
@@ -37,6 +47,7 @@ function ProxyClientTableMethodCalls() {
37
47
  const methodName = context.name;
38
48
  // WARNING: Using typescript's magical `this` param
39
49
  const proxyFn = function (...args) {
50
+ checkRemoteTableSaveOptions(methodName, args);
40
51
  const dataContextId = this.dataSource.dataContextId || this.groupId || "";
41
52
  return rpc_types_1.rpcServerCalls.tableMethodCall(dataContextId, this.tableName, methodName, ...args);
42
53
  };
@@ -57,10 +57,12 @@ export declare function checkLocalSave<T extends {
57
57
  */
58
58
  export declare const packageApprovalTables: ReadonlySet<string>;
59
59
  /**
60
- * What a table's `signAndSave` (and a re-sign) puts under the signature: `signedAt`, now or
61
- * just after the row on record when that was signed by a clock running ahead, so the new
62
- * signature counts as the later one (and, for a package table, after the deleted row's
63
- * retained stamp); for a package table also the context the approval is for.
60
+ * What a table's `signAndSave` (and a re-sign) puts under the signature: `signedAt`, a tick of
61
+ * the context's write clock (wall time without a tracked source) or just after the row on record
62
+ * when that was signed by a clock running ahead, so the new signature counts as the later one
63
+ * (and, for a package table, after the deleted row's retained stamp); for a package table also
64
+ * the context the approval is for. The save then records the change at this same time
65
+ * (`writtenAt`), so sync and the signed order agree on which concurrent write is later.
64
66
  *
65
67
  * @param table - The table being signed for.
66
68
  * @param row - The row being signed.
@@ -56,10 +56,12 @@ async function checkLocalSave(table, row, check) {
56
56
  */
57
57
  exports.packageApprovalTables = new Set(["Packages", "PackageVersions"]);
58
58
  /**
59
- * What a table's `signAndSave` (and a re-sign) puts under the signature: `signedAt`, now or
60
- * just after the row on record when that was signed by a clock running ahead, so the new
61
- * signature counts as the later one (and, for a package table, after the deleted row's
62
- * retained stamp); for a package table also the context the approval is for.
59
+ * What a table's `signAndSave` (and a re-sign) puts under the signature: `signedAt`, a tick of
60
+ * the context's write clock (wall time without a tracked source) or just after the row on record
61
+ * when that was signed by a clock running ahead, so the new signature counts as the later one
62
+ * (and, for a package table, after the deleted row's retained stamp); for a package table also
63
+ * the context the approval is for. The save then records the change at this same time
64
+ * (`writtenAt`), so sync and the signed order agree on which concurrent write is later.
63
65
  *
64
66
  * @param table - The table being signed for.
65
67
  * @param row - The row being signed.
@@ -76,8 +78,13 @@ async function signingStamp(table, row, current) {
76
78
  const dataContext = (await getUserContext()).getDataContext(groupId);
77
79
  floor = await deletedSignedAt(tableName, { dataContext, recordId });
78
80
  }
81
+ // Duck typing also works when the data source comes from another installed SDK copy.
82
+ let source = table.dataSource;
83
+ while (source && !source.getWriteClock)
84
+ source = source.dataSource;
85
+ const clock = await source?.getWriteClock?.();
79
86
  return {
80
- signedAt: Math.max(Date.now(), floor + 1),
87
+ signedAt: Math.max(clock ? Math.ceil(clock.tick()) : Date.now(), floor + 1),
81
88
  ...(packageTable ? { signedContextId: groupId } : {}),
82
89
  };
83
90
  }
@@ -137,7 +137,12 @@ let PackageVersionsTable = (() => {
137
137
  ...packageVersion,
138
138
  ...(await (0, write_checks_1.signingStamp)(this, packageVersion)),
139
139
  });
140
- return this.save(packageVersion, opts);
140
+ // Field patches from concurrent writes could combine into a row nobody signed.
141
+ return this.save(packageVersion, {
142
+ ...opts,
143
+ saveAsSnapshot: true,
144
+ writtenAt: packageVersion.signedAt,
145
+ });
141
146
  }
142
147
  /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
143
148
  static enablePackageVersionSigning(fn) {
@@ -127,7 +127,8 @@ let PackagesTable = (() => {
127
127
  ...packageObj,
128
128
  ...(await (0, write_checks_1.signingStamp)(this, packageObj)),
129
129
  });
130
- return this.save(packageObj, opts);
130
+ // Field patches from concurrent writes could combine into a row nobody signed.
131
+ return this.save(packageObj, { ...opts, saveAsSnapshot: true, writtenAt: packageObj.signedAt });
131
132
  }
132
133
  /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
133
134
  static enablePackageSigning(fn) {
@@ -9,18 +9,18 @@ export declare const tableDefinitionRecordSchema: z.ZodObject<{
9
9
  deleted: z.ZodOptional<z.ZodDate>;
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  name: string;
12
+ tableId: string;
12
13
  metaData: {} & {
13
14
  [k: string]: any;
14
15
  };
15
- tableId: string;
16
16
  versionNumber: number;
17
17
  deleted?: Date | undefined;
18
18
  }, {
19
19
  name: string;
20
+ tableId: string;
20
21
  metaData: {} & {
21
22
  [k: string]: any;
22
23
  };
23
- tableId: string;
24
24
  deleted?: Date | undefined;
25
25
  versionNumber?: number | undefined;
26
26
  }>;
@@ -36,10 +36,10 @@ export type ITableDefinitionRecord = z.infer<typeof tableDefinitionRecordSchema>
36
36
  */
37
37
  export declare function TableDefinitions(dataContext?: DataContext): import("..").Table<{
38
38
  name: string;
39
+ tableId: string;
39
40
  metaData: {} & {
40
41
  [k: string]: any;
41
42
  };
42
- tableId: string;
43
43
  versionNumber: number;
44
44
  deleted?: Date | undefined;
45
45
  }>;
@@ -46,9 +46,9 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
46
46
  }, "strip", z.ZodTypeAny, {
47
47
  nonce: string;
48
48
  status: ToolAccessApprovalStatus;
49
- userId: string;
50
49
  toolName: string;
51
50
  toolId: string;
51
+ userId: string;
52
52
  scope: ToolAccessApprovalScope;
53
53
  createdAt: Date;
54
54
  dataContextId: string;
@@ -69,9 +69,9 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
69
69
  resultSummary?: string | undefined;
70
70
  }, {
71
71
  nonce: string;
72
- userId: string;
73
72
  toolName: string;
74
73
  toolId: string;
74
+ userId: string;
75
75
  dataContextId: string;
76
76
  expiresAt: Date;
77
77
  toolAccessApprovalId: string;
@@ -102,9 +102,9 @@ export type IToolAccessApproval = z.infer<typeof toolAccessApprovalSchema>;
102
102
  export declare function ToolAccessApprovals(dataContext?: DataContext): import("..").Table<{
103
103
  nonce: string;
104
104
  status: ToolAccessApprovalStatus;
105
- userId: string;
106
105
  toolName: string;
107
106
  toolId: string;
107
+ userId: string;
108
108
  scope: ToolAccessApprovalScope;
109
109
  createdAt: Date;
110
110
  dataContextId: string;
@@ -18,6 +18,8 @@ export interface IPackageSaveOptions {
18
18
  /**
19
19
  * The save options a package may use, from whatever a caller passed: only `restoreIfDeleted`.
20
20
  * The host's other save options (snapshots, weak inserts) stay out of package hands.
21
+ * `writtenAt` must stay host-only even if this allowlist expands: it orders signed rows,
22
+ * and accepting it from a package would let that package backdate writes.
21
23
  *
22
24
  * @param options - A table `save`'s second argument, as received.
23
25
  * @returns The allowed options, or `undefined` when none are set.
@@ -18,6 +18,8 @@ exports.ISOLATED_CONTRACT_TABLE_OPERATIONS = [
18
18
  /**
19
19
  * The save options a package may use, from whatever a caller passed: only `restoreIfDeleted`.
20
20
  * The host's other save options (snapshots, weak inserts) stay out of package hands.
21
+ * `writtenAt` must stay host-only even if this allowlist expands: it orders signed rows,
22
+ * and accepting it from a package would let that package backdate writes.
21
23
  *
22
24
  * @param options - A table `save`'s second argument, as received.
23
25
  * @returns The allowed options, or `undefined` when none are set.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"