@peers-app/peers-sdk 0.23.2 → 0.23.5

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.
Files changed (68) hide show
  1. package/dist/context/data-context.d.ts +36 -5
  2. package/dist/context/data-context.js +44 -9
  3. package/dist/context/user-context.d.ts +44 -0
  4. package/dist/context/user-context.js +91 -12
  5. package/dist/contracts/__tests__/contract-provider-router.test.js +26 -0
  6. package/dist/contracts/__tests__/contract-resolvers.test.js +44 -0
  7. package/dist/contracts/__tests__/registry.test.js +104 -0
  8. package/dist/contracts/contract-provider-router.d.ts +21 -0
  9. package/dist/contracts/contract-provider-router.js +31 -5
  10. package/dist/contracts/contract-proxy.d.ts +3 -0
  11. package/dist/contracts/contract-proxy.js +3 -0
  12. package/dist/contracts/contract-resolvers.d.ts +21 -3
  13. package/dist/contracts/contract-resolvers.js +105 -32
  14. package/dist/contracts/index.d.ts +3 -3
  15. package/dist/contracts/registry.d.ts +59 -0
  16. package/dist/contracts/registry.js +155 -25
  17. package/dist/contracts/system-contracts.js +3 -1
  18. package/dist/data/group-share.d.ts +13 -0
  19. package/dist/data/group-share.js +32 -8
  20. package/dist/data/group-share.test.d.ts +1 -0
  21. package/dist/data/group-share.test.js +110 -0
  22. package/dist/data/index.d.ts +1 -0
  23. package/dist/data/index.js +1 -0
  24. package/dist/data/orm/types.d.ts +19 -0
  25. package/dist/data/orm/types.js +11 -0
  26. package/dist/data/package-table-identity.d.ts +46 -0
  27. package/dist/data/package-table-identity.js +71 -0
  28. package/dist/data/package-table-identity.test.d.ts +1 -0
  29. package/dist/data/package-table-identity.test.js +46 -0
  30. package/dist/data/package-version-resolver.d.ts +13 -0
  31. package/dist/data/package-version-resolver.js +142 -6
  32. package/dist/data/package-version-resolver.test.js +42 -0
  33. package/dist/data/persistent-var-identity.d.ts +37 -0
  34. package/dist/data/persistent-var-identity.js +69 -0
  35. package/dist/data/persistent-vars.d.ts +3 -5
  36. package/dist/data/persistent-vars.js +25 -13
  37. package/dist/data/persistent-vars.package-scope.test.d.ts +1 -0
  38. package/dist/data/persistent-vars.package-scope.test.js +27 -0
  39. package/dist/data/tools.d.ts +3 -3
  40. package/dist/group-invite/group-invite.types.d.ts +15 -5
  41. package/dist/package-loader/contract-package-loader.js +20 -8
  42. package/dist/package-loader/contract-package-loader.test.js +16 -0
  43. package/dist/package-loader/index.d.ts +4 -0
  44. package/dist/package-loader/index.js +4 -0
  45. package/dist/package-loader/isolated-artifact.d.ts +233 -0
  46. package/dist/package-loader/isolated-artifact.js +643 -0
  47. package/dist/package-loader/isolated-artifact.test.d.ts +1 -0
  48. package/dist/package-loader/isolated-artifact.test.js +270 -0
  49. package/dist/package-loader/isolated-persistent-var-gateway.d.ts +20 -0
  50. package/dist/package-loader/isolated-persistent-var-gateway.js +106 -0
  51. package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +1 -0
  52. package/dist/package-loader/isolated-persistent-var-gateway.test.js +56 -0
  53. package/dist/package-loader/isolated-runtime.d.ts +229 -0
  54. package/dist/package-loader/isolated-runtime.js +37 -0
  55. package/dist/package-loader/isolated-table-gateway.d.ts +16 -0
  56. package/dist/package-loader/isolated-table-gateway.js +375 -0
  57. package/dist/package-loader/isolated-table-gateway.test.d.ts +1 -0
  58. package/dist/package-loader/isolated-table-gateway.test.js +206 -0
  59. package/dist/package-loader/isolated-value.d.ts +14 -0
  60. package/dist/package-loader/isolated-value.js +63 -0
  61. package/dist/package-loader/package-loader.d.ts +123 -4
  62. package/dist/package-loader/package-loader.js +417 -46
  63. package/dist/package-loader/package-loader.test.js +478 -12
  64. package/dist/package-loader/resolve-consumed-contracts.d.ts +3 -3
  65. package/dist/package-loader/resolve-consumed-contracts.js +18 -10
  66. package/dist/utils.d.ts +21 -2
  67. package/dist/utils.js +46 -7
  68. package/package.json +1 -1
@@ -31,26 +31,139 @@ class ContractRegistry {
31
31
  * @returns Validation result. If invalid the provider is not registered.
32
32
  */
33
33
  register(providerPackageId, definition, alsoImplementsDecls = []) {
34
+ return this.registerBatch(providerPackageId, [
35
+ { definition, alsoImplements: alsoImplementsDecls },
36
+ ]);
37
+ }
38
+ /**
39
+ * Atomically register every contract supplied by one provider package.
40
+ *
41
+ * Frozen-shape and `alsoImplements` checks run against a candidate registry
42
+ * containing the whole batch, so declarations may reference another
43
+ * contract in the same install. If any entry fails, no definition, provider,
44
+ * active-provider pointer, or reverse index is changed.
45
+ *
46
+ * @param providerPackageId - Trusted identity of the provider package.
47
+ * @param registrations - Contract definitions and compatibility declarations.
48
+ * @returns The combined validation result.
49
+ */
50
+ registerBatch(providerPackageId, registrations) {
51
+ const validation = this.validateBatch(providerPackageId, registrations);
52
+ if (!validation.valid) {
53
+ return validation;
54
+ }
55
+ this.commitRegistrations(providerPackageId, registrations);
56
+ return validation;
57
+ }
58
+ /**
59
+ * Atomically replace the complete contract set for one provider package.
60
+ *
61
+ * Validation runs before the existing provider entries are touched. Active
62
+ * selections remain on this package for contract versions it still provides;
63
+ * removed versions fall back according to ordinary unregister semantics.
64
+ *
65
+ * @param providerPackageId - Trusted provider package identity.
66
+ * @param registrations - Complete replacement set.
67
+ * @returns Validation result; invalid replacements leave the registry unchanged.
68
+ */
69
+ replaceBatch(providerPackageId, registrations) {
70
+ const validation = this.validateBatch(providerPackageId, registrations);
71
+ if (!validation.valid) {
72
+ return validation;
73
+ }
74
+ const previouslyActive = new Set((this.packageContracts.get(providerPackageId) ?? []).filter((key) => this.activeProviders.get(key)?.providerPackageId === providerPackageId));
75
+ this.unregister(providerPackageId);
76
+ this.commitRegistrations(providerPackageId, registrations);
77
+ for (const key of previouslyActive) {
78
+ const provider = this.allProviders
79
+ .get(key)
80
+ ?.find((item) => item.providerPackageId === providerPackageId);
81
+ if (provider) {
82
+ this.activeProviders.set(key, provider);
83
+ }
84
+ }
85
+ return validation;
86
+ }
87
+ /**
88
+ * Validate a complete provider registration batch without changing registry state.
89
+ *
90
+ * @param providerPackageId - Trusted provider package identity.
91
+ * @param registrations - Definitions and compatibility declarations to validate.
92
+ * @returns Combined validation result.
93
+ */
94
+ validateBatch(providerPackageId, registrations) {
95
+ const candidateDefinitions = new Map(this.definitions);
96
+ const batchKeys = new Set();
97
+ for (const { definition } of registrations) {
98
+ const key = (0, types_1.contractKey)(definition.contractId, definition.version);
99
+ if (batchKeys.has(key)) {
100
+ return {
101
+ valid: false,
102
+ errors: [
103
+ {
104
+ kind: "contract",
105
+ name: key,
106
+ message: `Provider ${providerPackageId} registered contract ${key} more than once`,
107
+ },
108
+ ],
109
+ };
110
+ }
111
+ batchKeys.add(key);
112
+ const existingDef = this.definitions.get(key);
113
+ if (existingDef) {
114
+ const immutability = (0, validate_1.validateImmutability)(existingDef, definition);
115
+ if (!immutability.valid) {
116
+ return immutability;
117
+ }
118
+ }
119
+ candidateDefinitions.set(key, definition);
120
+ }
121
+ for (const { definition, alsoImplements = [] } of registrations) {
122
+ if (alsoImplements.length === 0) {
123
+ continue;
124
+ }
125
+ const compatibility = (0, validate_1.validateAlsoImplements)(definition, alsoImplements, (contractId, version) => candidateDefinitions.get((0, types_1.contractKey)(contractId, version)));
126
+ if (!compatibility.valid) {
127
+ return compatibility;
128
+ }
129
+ }
130
+ return { valid: true, errors: [] };
131
+ }
132
+ commitRegistrations(providerPackageId, registrations) {
133
+ const explicitKeys = new Set(registrations.map(({ definition }) => (0, types_1.contractKey)(definition.contractId, definition.version)));
134
+ for (const { definition } of registrations) {
135
+ this.commitProvider(providerPackageId, definition, definition);
136
+ }
137
+ for (const { definition, alsoImplements = [] } of registrations) {
138
+ for (const declaration of alsoImplements) {
139
+ const declaredVersion = declaration.version;
140
+ const versions = typeof declaredVersion === "number"
141
+ ? [declaredVersion]
142
+ : Array.from({ length: declaredVersion.to - declaredVersion.from + 1 }, (_, index) => declaredVersion.from + index);
143
+ for (const version of versions) {
144
+ const key = (0, types_1.contractKey)(declaration.contractId, version);
145
+ if (explicitKeys.has(key)) {
146
+ continue;
147
+ }
148
+ const target = this.definitions.get(key);
149
+ if (target) {
150
+ this.commitProvider(providerPackageId, target, definition);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ commitProvider(providerPackageId, definition, implementation) {
34
157
  const key = (0, types_1.contractKey)(definition.contractId, definition.version);
35
- // Immutability check against any previously stored definition
36
- const existingDef = this.definitions.get(key);
37
- if (existingDef) {
38
- const immResult = (0, validate_1.validateImmutability)(existingDef, definition);
39
- if (!immResult.valid)
40
- return immResult;
41
- }
42
- // alsoImplements check
43
- if (alsoImplementsDecls.length > 0) {
44
- const aiResult = (0, validate_1.validateAlsoImplements)(definition, alsoImplementsDecls, (cid, ver) => this.definitions.get((0, types_1.contractKey)(cid, ver)));
45
- if (!aiResult.valid)
46
- return aiResult;
47
- }
48
- // Store the definition (first-come for frozen, latest for dev)
49
158
  this.definitions.set(key, definition);
50
- const provider = { providerPackageId, definition };
51
- // Add to the all-providers list
159
+ const provider = {
160
+ providerPackageId,
161
+ definition,
162
+ implementationContractId: implementation.contractId,
163
+ implementationVersion: implementation.version,
164
+ };
52
165
  const providers = this.allProviders.get(key) ?? [];
53
- const existingIdx = providers.findIndex((p) => p.providerPackageId === providerPackageId);
166
+ const existingIdx = providers.findIndex((item) => item.providerPackageId === providerPackageId);
54
167
  if (existingIdx >= 0) {
55
168
  providers[existingIdx] = provider;
56
169
  }
@@ -58,17 +171,15 @@ class ContractRegistry {
58
171
  providers.push(provider);
59
172
  }
60
173
  this.allProviders.set(key, providers);
61
- // If no active provider, auto-activate
62
- if (!this.activeProviders.has(key)) {
174
+ const active = this.activeProviders.get(key);
175
+ if (!active || active.providerPackageId === providerPackageId) {
63
176
  this.activeProviders.set(key, provider);
64
177
  }
65
- // Update reverse index
66
- const pkgKeys = this.packageContracts.get(providerPackageId) ?? [];
67
- if (!pkgKeys.includes(key)) {
68
- pkgKeys.push(key);
178
+ const packageKeys = this.packageContracts.get(providerPackageId) ?? [];
179
+ if (!packageKeys.includes(key)) {
180
+ packageKeys.push(key);
69
181
  }
70
- this.packageContracts.set(providerPackageId, pkgKeys);
71
- return { valid: true, errors: [] };
182
+ this.packageContracts.set(providerPackageId, packageKeys);
72
183
  }
73
184
  /**
74
185
  * Switch the active provider for a contract to a different registered package.
@@ -106,6 +217,25 @@ class ContractRegistry {
106
217
  const key = (0, types_1.contractKey)(contractId, version);
107
218
  return (this.allProviders.get(key) ?? []).map((p) => p.providerPackageId);
108
219
  }
220
+ /**
221
+ * Resolve the concrete contract implementation selected for one provider.
222
+ *
223
+ * For a direct registration this matches the requested contract. For an
224
+ * `alsoImplements` alias it identifies the newer compatible implementation.
225
+ *
226
+ * @param contractId - Consumer-requested contract identity.
227
+ * @param version - Consumer-requested contract version.
228
+ * @param providerPackageId - Registry-selected provider package.
229
+ */
230
+ getProviderImplementation(contractId, version, providerPackageId) {
231
+ const provider = (this.allProviders.get((0, types_1.contractKey)(contractId, version)) ?? []).find((candidate) => candidate.providerPackageId === providerPackageId);
232
+ return provider
233
+ ? {
234
+ contractId: provider.implementationContractId,
235
+ version: provider.implementationVersion,
236
+ }
237
+ : undefined;
238
+ }
109
239
  /**
110
240
  * Pick a provider for a consumed-contract declaration.
111
241
  *
@@ -25,7 +25,9 @@ function registerSystemContract(registration) {
25
25
  const { contractId, version, resolve } = registration;
26
26
  queueMicrotask(() => {
27
27
  const { registerContractResolver } = require("./contract-resolvers");
28
- registerContractResolver(contractId, version, resolve);
28
+ registerContractResolver(contractId, version, resolve, {
29
+ providerPackageId: registration.providerPackageId,
30
+ });
29
31
  });
30
32
  }
31
33
  const key = registration.contractId && registration.version != null
@@ -1,3 +1,4 @@
1
+ import type { UserContext } from "../context/user-context";
1
2
  import { type IGroupMember } from "./group-members";
2
3
  import { type IGroup } from "./groups";
3
4
  /**
@@ -16,6 +17,18 @@ export interface IGroupShare {
16
17
  * @throws Error if group not found or user lacks permission
17
18
  */
18
19
  export declare function generateGroupShare(groupId: string): Promise<IGroupShare>;
20
+ /**
21
+ * Write a group record and missing membership rows into the group's data context.
22
+ *
23
+ * Uses the lowest data source so bootstrap can seed already-signed rows without
24
+ * re-running table permission checks. Existing memberships for the same user
25
+ * are left in place so retries can repair a partial import.
26
+ *
27
+ * @param userContext - Joiner or importer whose group context should be seeded
28
+ * @param groupShare - Canonical group plus Admin+ memberships
29
+ * @param extraMembers - Additional signed rows such as a newly approved Reader
30
+ */
31
+ export declare function seedGroupContextFromShare(userContext: UserContext, groupShare: Pick<IGroupShare, "group" | "groupMembers">, extraMembers?: IGroupMember[]): Promise<void>;
19
32
  /**
20
33
  * Validates that an object has the correct GroupShare structure
21
34
  * @param obj Object to validate
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateGroupShare = generateGroupShare;
4
+ exports.seedGroupContextFromShare = seedGroupContextFromShare;
4
5
  exports.validateGroupShare = validateGroupShare;
5
6
  exports.estimateGroupShareSize = estimateGroupShareSize;
6
7
  exports.checkQRCodeCompatibility = checkQRCodeCompatibility;
@@ -57,7 +58,7 @@ async function importGroupShare(groupShareJson) {
57
58
  throw new Error("Invalid GroupShare structure");
58
59
  }
59
60
  const userContext = await (0, user_context_singleton_1.getUserContext)();
60
- const { group, groupMembers } = groupShare;
61
+ const { group } = groupShare;
61
62
  // Check if group already exists
62
63
  const existingGroup = await (0, groups_1.Groups)(userContext.userDataContext).get(group.groupId);
63
64
  if (existingGroup) {
@@ -65,19 +66,42 @@ async function importGroupShare(groupShareJson) {
65
66
  }
66
67
  // Save group to user's groups table
67
68
  await (0, groups_1.Groups)(userContext.userDataContext).save(group, { restoreIfDeleted: true });
68
- // Get group data context for the new group
69
+ await seedGroupContextFromShare(userContext, groupShare);
70
+ return group.groupId;
71
+ }
72
+ /**
73
+ * Write a group record and missing membership rows into the group's data context.
74
+ *
75
+ * Uses the lowest data source so bootstrap can seed already-signed rows without
76
+ * re-running table permission checks. Existing memberships for the same user
77
+ * are left in place so retries can repair a partial import.
78
+ *
79
+ * @param userContext - Joiner or importer whose group context should be seeded
80
+ * @param groupShare - Canonical group plus Admin+ memberships
81
+ * @param extraMembers - Additional signed rows such as a newly approved Reader
82
+ */
83
+ async function seedGroupContextFromShare(userContext, groupShare, extraMembers = []) {
84
+ const { group, groupMembers } = groupShare;
69
85
  const groupDataContext = userContext.getDataContext(group.groupId);
70
- // Save the group data to the group's own context
71
86
  const groupsTable = (0, groups_1.Groups)(groupDataContext);
72
- const groupsTableDataSource = getLowestDataSource(groupsTable);
73
- await groupsTableDataSource.save(group);
74
- // Save all admin members as seed data for device recognition
87
+ await getLowestDataSource(groupsTable).save(group);
75
88
  const groupMembersTable = (0, group_members_1.GroupMembers)(groupDataContext);
76
89
  const groupMembersDataSource = getLowestDataSource(groupMembersTable);
77
- for (const member of groupMembers) {
90
+ const seenUserIds = new Set();
91
+ for (const member of [...groupMembers, ...extraMembers]) {
92
+ if (!member?.userId || member.groupId !== group.groupId || seenUserIds.has(member.userId)) {
93
+ continue;
94
+ }
95
+ seenUserIds.add(member.userId);
96
+ const existing = await groupMembersTable.findOne({
97
+ groupId: group.groupId,
98
+ userId: member.userId,
99
+ });
100
+ if (existing) {
101
+ continue;
102
+ }
78
103
  await groupMembersDataSource.save(member);
79
104
  }
80
- return group.groupId;
81
105
  }
82
106
  /**
83
107
  * Validates that an object has the correct GroupShare structure
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SQLiteDB = require("better-sqlite3");
4
+ const user_context_1 = require("../context/user-context");
5
+ const utils_1 = require("../utils");
6
+ const group_member_roles_1 = require("./group-member-roles");
7
+ const group_members_1 = require("./group-members");
8
+ const group_share_1 = require("./group-share");
9
+ const groups_1 = require("./groups");
10
+ const sql_data_source_1 = require("./orm/sql.data-source");
11
+ class DBHarness {
12
+ _db = null;
13
+ get db() {
14
+ if (!this._db) {
15
+ this._db = new SQLiteDB(":memory:");
16
+ this._db.pragma("journal_mode = WAL");
17
+ }
18
+ return this._db;
19
+ }
20
+ async get(sql, params = []) {
21
+ return this.db.prepare(sql).get(params);
22
+ }
23
+ async all(sql, params = []) {
24
+ return this.db.prepare(sql).all(params);
25
+ }
26
+ async exec(sql, params = []) {
27
+ const result = this.db.prepare(sql).run(params);
28
+ return { changes: result.changes };
29
+ }
30
+ async close() {
31
+ this._db?.close();
32
+ this._db = null;
33
+ }
34
+ }
35
+ describe("seedGroupContextFromShare", () => {
36
+ const dbs = new Map();
37
+ let userContext;
38
+ const userId = (0, utils_1.newid)();
39
+ beforeAll(async () => {
40
+ groups_1.GroupsTable.isPassthrough = true;
41
+ group_members_1.GroupMembersTable.isPassthrough = true;
42
+ const dataSourceFactory = (metaData, schema, groupId) => {
43
+ const key = groupId || userId;
44
+ let db = dbs.get(key);
45
+ if (!db) {
46
+ db = new DBHarness();
47
+ dbs.set(key, db);
48
+ }
49
+ return new sql_data_source_1.SQLDataSource(db, metaData, schema);
50
+ };
51
+ userContext = new user_context_1.UserContext(userId, dataSourceFactory, true);
52
+ await userContext.loadingPromise;
53
+ userContext.deviceId((0, utils_1.newid)());
54
+ });
55
+ afterAll(async () => {
56
+ groups_1.GroupsTable.isPassthrough = false;
57
+ group_members_1.GroupMembersTable.isPassthrough = false;
58
+ userContext.dispose();
59
+ await Promise.all([...dbs.values()].map((db) => db.close()));
60
+ });
61
+ function makeGroup() {
62
+ return {
63
+ groupId: (0, utils_1.newid)(),
64
+ name: "Home",
65
+ description: "",
66
+ founderUserId: (0, utils_1.newid)(),
67
+ signature: "group-sig",
68
+ publicKey: "pk",
69
+ publicBoxKey: "pbk",
70
+ };
71
+ }
72
+ function makeMember(groupId, memberUserId, role) {
73
+ return {
74
+ groupMemberId: (0, utils_1.newid)(),
75
+ groupId,
76
+ userId: memberUserId,
77
+ role,
78
+ signature: "member-sig",
79
+ };
80
+ }
81
+ it("seeds the group record and missing memberships, including extra Reader rows", async () => {
82
+ const group = makeGroup();
83
+ const admin = makeMember(group.groupId, (0, utils_1.newid)(), group_member_roles_1.GroupMemberRole.Admin);
84
+ const joiner = makeMember(group.groupId, userId, group_member_roles_1.GroupMemberRole.Reader);
85
+ await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [admin] }, [joiner]);
86
+ const groupContext = userContext.getDataContext(group.groupId);
87
+ await expect((0, groups_1.Groups)(groupContext).get(group.groupId)).resolves.toMatchObject({
88
+ name: "Home",
89
+ });
90
+ await expect((0, group_members_1.GroupMembers)(groupContext).findOne({ userId: admin.userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Admin });
91
+ await expect((0, group_members_1.GroupMembers)(groupContext).findOne({ userId: userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Reader });
92
+ });
93
+ it("leaves existing memberships in place on a retry", async () => {
94
+ const group = makeGroup();
95
+ const admin = makeMember(group.groupId, (0, utils_1.newid)(), group_member_roles_1.GroupMemberRole.Admin);
96
+ await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [admin] });
97
+ const replacement = {
98
+ ...admin,
99
+ groupMemberId: (0, utils_1.newid)(),
100
+ signature: "other-sig",
101
+ };
102
+ await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [replacement] });
103
+ const saved = await (0, group_members_1.GroupMembers)(userContext.getDataContext(group.groupId)).findOne({
104
+ userId: admin.userId,
105
+ groupId: group.groupId,
106
+ });
107
+ expect(saved?.groupMemberId).toBe(admin.groupMemberId);
108
+ expect(saved?.signature).toBe("member-sig");
109
+ });
110
+ });
@@ -13,6 +13,7 @@ export * from "./group-secret";
13
13
  export * from "./group-share";
14
14
  export * from "./groups";
15
15
  export * from "./messages";
16
+ export * from "./package-table-identity";
16
17
  export * from "./package-version-resolver";
17
18
  export * from "./package-versions";
18
19
  export * from "./packages";
@@ -29,6 +29,7 @@ __exportStar(require("./group-secret"), exports);
29
29
  __exportStar(require("./group-share"), exports);
30
30
  __exportStar(require("./groups"), exports);
31
31
  __exportStar(require("./messages"), exports);
32
+ __exportStar(require("./package-table-identity"), exports);
32
33
  __exportStar(require("./package-version-resolver"), exports);
33
34
  __exportStar(require("./package-versions"), exports);
34
35
  __exportStar(require("./packages"), exports);
@@ -1,7 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { type IField } from "../../types/field-type";
3
+ /** Pure-data schema and storage metadata for a Peers table. */
3
4
  export interface ITableMetaData {
4
5
  tableId?: string;
6
+ /**
7
+ * Trusted owning package for package-scoped physical naming.
8
+ *
9
+ * When present, physical naming uses `name_packageId` while `tableId`
10
+ * remains the independent schema-definition identity.
11
+ */
12
+ packageId?: string;
5
13
  name: string;
6
14
  description: string;
7
15
  primaryKeyName: string;
@@ -17,5 +25,16 @@ export interface ITableMetaData {
17
25
  localOnly?: boolean;
18
26
  versionNumber?: number;
19
27
  }
28
+ /**
29
+ * Convert field metadata to the runtime Zod schema used by table validation.
30
+ *
31
+ * @param fields Serializable field definitions.
32
+ * @param codeSchema Optional author schema whose refinements are intersected.
33
+ */
20
34
  export declare function fieldsToSchema(fields: IField[], codeSchema?: z.AnyZodObject): z.AnyZodObject;
35
+ /**
36
+ * Convert a Zod object schema to serializable field metadata.
37
+ *
38
+ * @param schema Runtime record schema.
39
+ */
21
40
  export declare function schemaToFields(schema: z.AnyZodObject): IField[];
@@ -5,6 +5,12 @@ exports.schemaToFields = schemaToFields;
5
5
  const zod_1 = require("zod");
6
6
  const field_type_1 = require("../../types/field-type");
7
7
  const utils_1 = require("../../utils");
8
+ /**
9
+ * Convert field metadata to the runtime Zod schema used by table validation.
10
+ *
11
+ * @param fields Serializable field definitions.
12
+ * @param codeSchema Optional author schema whose refinements are intersected.
13
+ */
8
14
  function fieldsToSchema(fields, codeSchema) {
9
15
  let argsSchema = zod_1.z.object({});
10
16
  for (const argSchema of fields) {
@@ -52,6 +58,11 @@ function fieldsToSchema(fields, codeSchema) {
52
58
  }
53
59
  return argsSchema;
54
60
  }
61
+ /**
62
+ * Convert a Zod object schema to serializable field metadata.
63
+ *
64
+ * @param schema Runtime record schema.
65
+ */
55
66
  function schemaToFields(schema) {
56
67
  const fields = [];
57
68
  for (const name of Object.keys(schema.shape)) {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Validate a guest-supplied isolated-table or field identifier before it can
3
+ * reach ORM-generated SQL.
4
+ *
5
+ * This intentionally accepts only simple SQLite identifiers. The package
6
+ * manifest validator and host gateway can share this helper so malformed
7
+ * names are rejected even when a declaration bypasses artifact loading.
8
+ *
9
+ * @param identifier Guest-supplied logical table or field name.
10
+ * @param label Description used in validation errors.
11
+ * @returns The unchanged identifier.
12
+ * @throws When the identifier is empty or contains SQL syntax.
13
+ */
14
+ export declare function validateIsolatedSqlIdentifier(identifier: string, label?: string): string;
15
+ /**
16
+ * Validate a package-local table name.
17
+ *
18
+ * @param logicalName Guest-visible table name.
19
+ * @returns The unchanged logical name.
20
+ * @throws When the name is not a SQL-safe isolated identifier.
21
+ */
22
+ export declare function validatePackageTableName(logicalName: string): string;
23
+ /**
24
+ * Derive the physical SQLite name for a package-owned table.
25
+ *
26
+ * Package ID is appended so authors only need unique logical names inside
27
+ * their own package.
28
+ *
29
+ * @param packageId Trusted owning package ID.
30
+ * @param logicalName Package-local table name.
31
+ * @returns The package-scoped physical table name.
32
+ * @throws When package or table identity is invalid.
33
+ */
34
+ export declare function packageScopedTableName(packageId: string, logicalName: string): string;
35
+ /**
36
+ * Derive the stable host-only ID used to key `TableDefinitions`.
37
+ *
38
+ * The ID is intentionally separate from the physical SQLite name so schema
39
+ * tracking keeps a valid peer ID without producing a triple-suffixed table.
40
+ *
41
+ * @param packageId Trusted owning package ID.
42
+ * @param logicalName Package-local table name.
43
+ * @returns A deterministic peer ID for the package-owned table definition.
44
+ * @throws When package or table identity is invalid.
45
+ */
46
+ export declare function packageScopedTableId(packageId: string, logicalName: string): string;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateIsolatedSqlIdentifier = validateIsolatedSqlIdentifier;
4
+ exports.validatePackageTableName = validatePackageTableName;
5
+ exports.packageScopedTableName = packageScopedTableName;
6
+ exports.packageScopedTableId = packageScopedTableId;
7
+ const utils_1 = require("../utils");
8
+ const ISOLATED_SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
+ /**
10
+ * Validate a guest-supplied isolated-table or field identifier before it can
11
+ * reach ORM-generated SQL.
12
+ *
13
+ * This intentionally accepts only simple SQLite identifiers. The package
14
+ * manifest validator and host gateway can share this helper so malformed
15
+ * names are rejected even when a declaration bypasses artifact loading.
16
+ *
17
+ * @param identifier Guest-supplied logical table or field name.
18
+ * @param label Description used in validation errors.
19
+ * @returns The unchanged identifier.
20
+ * @throws When the identifier is empty or contains SQL syntax.
21
+ */
22
+ function validateIsolatedSqlIdentifier(identifier, label = "Isolated SQL identifier") {
23
+ if (typeof identifier !== "string" || identifier.trim() === "") {
24
+ throw new Error(`${label} must be a non-empty string`);
25
+ }
26
+ if (!ISOLATED_SQL_IDENTIFIER.test(identifier)) {
27
+ throw new Error(`${label} must start with a letter or underscore and contain only letters, numbers, and underscores`);
28
+ }
29
+ return identifier;
30
+ }
31
+ /**
32
+ * Validate a package-local table name.
33
+ *
34
+ * @param logicalName Guest-visible table name.
35
+ * @returns The unchanged logical name.
36
+ * @throws When the name is not a SQL-safe isolated identifier.
37
+ */
38
+ function validatePackageTableName(logicalName) {
39
+ return validateIsolatedSqlIdentifier(logicalName, "Package table name");
40
+ }
41
+ /**
42
+ * Derive the physical SQLite name for a package-owned table.
43
+ *
44
+ * Package ID is appended so authors only need unique logical names inside
45
+ * their own package.
46
+ *
47
+ * @param packageId Trusted owning package ID.
48
+ * @param logicalName Package-local table name.
49
+ * @returns The package-scoped physical table name.
50
+ * @throws When package or table identity is invalid.
51
+ */
52
+ function packageScopedTableName(packageId, logicalName) {
53
+ if (!(0, utils_1.isid)(packageId)) {
54
+ throw new Error(`Invalid package ID for table: ${packageId}`);
55
+ }
56
+ return `${validatePackageTableName(logicalName)}_${packageId}`;
57
+ }
58
+ /**
59
+ * Derive the stable host-only ID used to key `TableDefinitions`.
60
+ *
61
+ * The ID is intentionally separate from the physical SQLite name so schema
62
+ * tracking keeps a valid peer ID without producing a triple-suffixed table.
63
+ *
64
+ * @param packageId Trusted owning package ID.
65
+ * @param logicalName Package-local table name.
66
+ * @returns A deterministic peer ID for the package-owned table definition.
67
+ * @throws When package or table identity is invalid.
68
+ */
69
+ function packageScopedTableId(packageId, logicalName) {
70
+ return (0, utils_1.deterministicTableId)(packageScopedTableName(packageId, logicalName));
71
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("../utils");
4
+ const package_table_identity_1 = require("./package-table-identity");
5
+ describe("package-scoped table identity", () => {
6
+ test("appends trusted package identity to the logical table name", () => {
7
+ const packageId = (0, utils_1.newid)();
8
+ const tableId = (0, package_table_identity_1.packageScopedTableId)(packageId, "Settings");
9
+ const metaData = {
10
+ packageId,
11
+ tableId,
12
+ name: "Settings",
13
+ description: "Package settings",
14
+ primaryKeyName: "settingId",
15
+ fields: [],
16
+ };
17
+ expect((0, package_table_identity_1.packageScopedTableName)(packageId, "Settings")).toBe(`Settings_${packageId}`);
18
+ expect((0, utils_1.getFullTableName)(metaData)).toBe(`Settings_${packageId}`);
19
+ expect((0, utils_1.getFullTableName)(metaData)).not.toContain(tableId);
20
+ expect((0, utils_1.isid)(tableId)).toBe(true);
21
+ expect((0, utils_1.isDeterministicTableId)(tableId)).toBe(true);
22
+ });
23
+ test("keeps existing table naming unchanged", () => {
24
+ const tableId = (0, utils_1.newid)();
25
+ expect((0, utils_1.getFullTableName)({
26
+ tableId,
27
+ name: "Settings",
28
+ description: "Existing table",
29
+ primaryKeyName: "settingId",
30
+ fields: [],
31
+ })).toBe(`Settings_${tableId}`);
32
+ });
33
+ test("uses package and logical name for stable internal IDs", () => {
34
+ const packageId = (0, utils_1.newid)();
35
+ expect((0, package_table_identity_1.packageScopedTableId)(packageId, "One")).toBe((0, package_table_identity_1.packageScopedTableId)(packageId, "One"));
36
+ expect((0, package_table_identity_1.packageScopedTableId)(packageId, "One")).not.toBe((0, package_table_identity_1.packageScopedTableId)(packageId, "Two"));
37
+ expect((0, package_table_identity_1.packageScopedTableId)(packageId, "One")).not.toBe((0, package_table_identity_1.packageScopedTableId)((0, utils_1.newid)(), "One"));
38
+ });
39
+ test("rejects invalid package IDs and empty logical names", () => {
40
+ expect(() => (0, package_table_identity_1.packageScopedTableName)("not-an-id", "Settings")).toThrow(/package ID/);
41
+ expect(() => (0, package_table_identity_1.packageScopedTableName)((0, utils_1.newid)(), " ")).toThrow(/non-empty/);
42
+ expect(() => (0, package_table_identity_1.packageScopedTableName)((0, utils_1.newid)(), 'State"; DROP TABLE Users; --')).toThrow(/only letters, numbers, and underscores/);
43
+ expect((0, package_table_identity_1.validateIsolatedSqlIdentifier)("state_value", "field")).toBe("state_value");
44
+ expect(() => (0, package_table_identity_1.validateIsolatedSqlIdentifier)("state.value", "field")).toThrow(/field/);
45
+ });
46
+ });