@hasna/contacts 0.6.33 → 0.6.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -144,7 +144,10 @@ function uuid() {
144
144
  return crypto.randomUUID();
145
145
  }
146
146
  function now() {
147
- return new Date().toISOString();
147
+ const currentMs = Date.now();
148
+ const nextMs = currentMs > lastNowMs ? currentMs : lastNowMs + 1;
149
+ lastNowMs = nextMs;
150
+ return new Date(nextMs).toISOString();
148
151
  }
149
152
  function quoteIdentifier(identifier) {
150
153
  return `"${identifier.replaceAll('"', '""')}"`;
@@ -182,7 +185,7 @@ function runMigrations(db) {
182
185
  db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${i})`);
183
186
  }
184
187
  }
185
- var MIGRATIONS, _db = null;
188
+ var MIGRATIONS, _db = null, lastNowMs = 0;
186
189
  var init_database = __esm(() => {
187
190
  init_sqlite_adapter();
188
191
  init_paths();
@@ -754,6 +757,81 @@ var init_database = __esm(() => {
754
757
  );
755
758
 
756
759
  CREATE INDEX IF NOT EXISTS idx_contacts_tombstones_deleted_at ON _contacts_tombstones(deleted_at);
760
+ `,
761
+ `
762
+ CREATE TABLE IF NOT EXISTS contact_project_membership_states (
763
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
764
+ project_id TEXT NOT NULL,
765
+ linked INTEGER NOT NULL CHECK(linked IN (0, 1)),
766
+ revision INTEGER NOT NULL DEFAULT 0 CHECK(revision >= 0),
767
+ updated_at TEXT NOT NULL,
768
+ PRIMARY KEY (contact_id, project_id)
769
+ );
770
+
771
+ INSERT OR IGNORE INTO contact_project_membership_states
772
+ (contact_id, project_id, linked, revision, updated_at)
773
+ SELECT contact_id, project_id, 1, 0, datetime('now')
774
+ FROM contact_projects;
775
+
776
+ CREATE TABLE IF NOT EXISTS contact_project_membership_receipts (
777
+ receipt_id TEXT PRIMARY KEY,
778
+ direction TEXT NOT NULL CHECK(direction IN ('attach', 'detach')),
779
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
780
+ project_id TEXT NOT NULL,
781
+ operation_id TEXT NOT NULL,
782
+ step_id TEXT NOT NULL,
783
+ expected_version TEXT NOT NULL,
784
+ before_json TEXT NOT NULL,
785
+ after_json TEXT NOT NULL,
786
+ created_at TEXT NOT NULL,
787
+ UNIQUE(operation_id, step_id)
788
+ );
789
+
790
+ CREATE INDEX IF NOT EXISTS idx_contact_project_membership_states_project
791
+ ON contact_project_membership_states(project_id, linked, contact_id);
792
+ CREATE INDEX IF NOT EXISTS idx_contact_project_membership_receipts_target
793
+ ON contact_project_membership_receipts(contact_id, project_id, created_at);
794
+
795
+ CREATE TRIGGER IF NOT EXISTS sync_contact_project_membership_state_after_insert
796
+ AFTER INSERT ON contact_projects
797
+ BEGIN
798
+ INSERT INTO contact_project_membership_states
799
+ (contact_id, project_id, linked, revision, updated_at)
800
+ VALUES (NEW.contact_id, NEW.project_id, 1, 1, datetime('now'))
801
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
802
+ linked = 1,
803
+ revision = CASE
804
+ WHEN contact_project_membership_states.linked = 1
805
+ THEN contact_project_membership_states.revision
806
+ ELSE contact_project_membership_states.revision + 1
807
+ END,
808
+ updated_at = CASE
809
+ WHEN contact_project_membership_states.linked = 1
810
+ THEN contact_project_membership_states.updated_at
811
+ ELSE datetime('now')
812
+ END;
813
+ END;
814
+
815
+ CREATE TRIGGER IF NOT EXISTS sync_contact_project_membership_state_after_delete
816
+ AFTER DELETE ON contact_projects
817
+ WHEN EXISTS (SELECT 1 FROM contacts WHERE id = OLD.contact_id)
818
+ BEGIN
819
+ INSERT INTO contact_project_membership_states
820
+ (contact_id, project_id, linked, revision, updated_at)
821
+ VALUES (OLD.contact_id, OLD.project_id, 0, 1, datetime('now'))
822
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
823
+ linked = 0,
824
+ revision = CASE
825
+ WHEN contact_project_membership_states.linked = 0
826
+ THEN contact_project_membership_states.revision
827
+ ELSE contact_project_membership_states.revision + 1
828
+ END,
829
+ updated_at = CASE
830
+ WHEN contact_project_membership_states.linked = 0
831
+ THEN contact_project_membership_states.updated_at
832
+ ELSE datetime('now')
833
+ END;
834
+ END;
757
835
  `
758
836
  ];
759
837
  });
@@ -853,6 +931,219 @@ var init_tombstones = __esm(() => {
853
931
  init_database();
854
932
  });
855
933
 
934
+ // src/types/project-memberships.ts
935
+ var ContactProjectMembershipConflictError;
936
+ var init_project_memberships = __esm(() => {
937
+ ContactProjectMembershipConflictError = class ContactProjectMembershipConflictError extends Error {
938
+ constructor(message) {
939
+ super(message);
940
+ this.name = "ContactProjectMembershipConflictError";
941
+ }
942
+ };
943
+ });
944
+
945
+ // src/db/project-memberships.ts
946
+ import { createHash } from "crypto";
947
+ function digest(value) {
948
+ return createHash("sha256").update(value).digest("hex");
949
+ }
950
+ function membershipVersion(contactId, projectId, linked, revision) {
951
+ return `cpmv_${digest(JSON.stringify([contactId, projectId, linked, revision])).slice(0, 32)}`;
952
+ }
953
+ function receiptId(input) {
954
+ return `cpmr_${digest(JSON.stringify([
955
+ input.operation_id,
956
+ input.step_id,
957
+ input.contact_id,
958
+ input.project_id
959
+ ])).slice(0, 32)}`;
960
+ }
961
+ function stateRow(contactId, projectId, db) {
962
+ const persisted = db.query(`SELECT contact_id, project_id, linked, revision
963
+ FROM contact_project_membership_states
964
+ WHERE contact_id = ? AND project_id = ?`).get(contactId, projectId);
965
+ if (persisted)
966
+ return persisted;
967
+ const linked = Boolean(db.query(`SELECT 1 AS present FROM contact_projects WHERE contact_id = ? AND project_id = ?`).get(contactId, projectId));
968
+ return { contact_id: contactId, project_id: projectId, linked: linked ? 1 : 0, revision: 0 };
969
+ }
970
+ function snapshot(row) {
971
+ const linked = Boolean(row.linked);
972
+ return {
973
+ contact_id: row.contact_id,
974
+ project_id: row.project_id,
975
+ linked,
976
+ version: membershipVersion(row.contact_id, row.project_id, linked, row.revision)
977
+ };
978
+ }
979
+ function required(value, name) {
980
+ const normalized = value.trim();
981
+ if (!normalized)
982
+ throw new Error(`${name} is required`);
983
+ return normalized;
984
+ }
985
+ function normalizeInput(input) {
986
+ return {
987
+ contact_id: required(input.contact_id, "contact_id"),
988
+ project_id: required(input.project_id, "project_id"),
989
+ operation_id: required(input.operation_id, "operation_id"),
990
+ step_id: required(input.step_id, "step_id"),
991
+ expected_version: required(input.expected_version, "expected_version")
992
+ };
993
+ }
994
+ function parseSnapshot(value) {
995
+ return JSON.parse(value);
996
+ }
997
+ function replay(row, direction, input) {
998
+ if (row.direction !== direction || row.contact_id !== input.contact_id || row.project_id !== input.project_id || row.expected_version !== input.expected_version) {
999
+ throw new ContactProjectMembershipConflictError(`operation_id/step_id already accepted for a different contact-project membership mutation`);
1000
+ }
1001
+ return {
1002
+ outcome: "duplicate_of_accepted",
1003
+ operation_id: row.operation_id,
1004
+ step_id: row.step_id,
1005
+ before: parseSnapshot(row.before_json),
1006
+ after: parseSnapshot(row.after_json),
1007
+ receipt_id: row.receipt_id
1008
+ };
1009
+ }
1010
+ function transitionWithoutReceipt(contactId, projectId, linked, db) {
1011
+ const beforeRow = stateRow(contactId, projectId, db);
1012
+ const changed = Boolean(beforeRow.linked) !== linked;
1013
+ const afterRow = {
1014
+ ...beforeRow,
1015
+ linked: linked ? 1 : 0,
1016
+ revision: changed ? beforeRow.revision + 1 : beforeRow.revision
1017
+ };
1018
+ const changedAt = now();
1019
+ db.run(`INSERT INTO contact_project_membership_states
1020
+ (contact_id, project_id, linked, revision, updated_at)
1021
+ VALUES (?, ?, ?, ?, ?)
1022
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
1023
+ linked = excluded.linked,
1024
+ revision = excluded.revision,
1025
+ updated_at = excluded.updated_at`, [contactId, projectId, afterRow.linked, afterRow.revision, changedAt]);
1026
+ if (linked) {
1027
+ db.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, projectId]);
1028
+ } else {
1029
+ db.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [contactId, projectId]);
1030
+ }
1031
+ return changed;
1032
+ }
1033
+ function setContactProjectMembershipWithoutReceipt(contactId, projectId, linked, db = getDatabase()) {
1034
+ const normalizedContactId = required(contactId, "contact_id");
1035
+ const normalizedProjectId = required(projectId, "project_id");
1036
+ return db.transaction(() => transitionWithoutReceipt(normalizedContactId, normalizedProjectId, linked, db));
1037
+ }
1038
+ function replaceContactProjectMembershipsWithoutReceipts(contactId, projectIds, db = getDatabase()) {
1039
+ const normalizedContactId = required(contactId, "contact_id");
1040
+ const normalizedProjectIds = [...new Set(projectIds.map((projectId) => required(projectId, "project_id")))];
1041
+ return db.transaction(() => {
1042
+ const currentRows = db.query(`SELECT project_id FROM contact_projects WHERE contact_id = ?`).all(normalizedContactId);
1043
+ const desired = new Set(normalizedProjectIds);
1044
+ const population = new Set([...currentRows.map((row) => row.project_id), ...normalizedProjectIds]);
1045
+ for (const projectId of population) {
1046
+ transitionWithoutReceipt(normalizedContactId, projectId, desired.has(projectId), db);
1047
+ }
1048
+ return normalizedProjectIds;
1049
+ });
1050
+ }
1051
+ function readContactProjectMembership(contactId, projectId, db = getDatabase()) {
1052
+ return snapshot(stateRow(required(contactId, "contact_id"), required(projectId, "project_id"), db));
1053
+ }
1054
+ function listContactProjectMemberships(projectId, maxItems, db = getDatabase()) {
1055
+ const normalizedProjectId = required(projectId, "project_id");
1056
+ if (!Number.isInteger(maxItems) || maxItems < 1)
1057
+ throw new Error("max_items must be a positive integer");
1058
+ const rows = db.query(`SELECT cp.contact_id, cp.project_id,
1059
+ COALESCE(state.linked, 1) AS linked,
1060
+ COALESCE(state.revision, 0) AS revision
1061
+ FROM contact_projects cp
1062
+ LEFT JOIN contact_project_membership_states state
1063
+ ON state.contact_id = cp.contact_id AND state.project_id = cp.project_id
1064
+ WHERE cp.project_id = ?
1065
+ ORDER BY cp.contact_id ASC
1066
+ LIMIT ?`).all(normalizedProjectId, maxItems + 1);
1067
+ if (rows.length > maxItems) {
1068
+ throw new Error(`contact project membership collection exceeds max_items=${maxItems}`);
1069
+ }
1070
+ const contactIds = rows.map((row) => row.contact_id);
1071
+ return {
1072
+ project_id: normalizedProjectId,
1073
+ contact_ids: contactIds,
1074
+ complete: true,
1075
+ membership_revision: `cpml_${digest(JSON.stringify(rows.map((row) => snapshot(row)))).slice(0, 32)}`
1076
+ };
1077
+ }
1078
+ function mutateContactProjectMembership(direction, rawInput, db = getDatabase()) {
1079
+ const input = normalizeInput(rawInput);
1080
+ return db.transaction(() => {
1081
+ const existingReceipt = db.query(`SELECT direction, contact_id, project_id, operation_id, step_id, expected_version,
1082
+ before_json, after_json, receipt_id
1083
+ FROM contact_project_membership_receipts
1084
+ WHERE operation_id = ? AND step_id = ?`).get(input.operation_id, input.step_id);
1085
+ if (existingReceipt)
1086
+ return replay(existingReceipt, direction, input);
1087
+ const contact = db.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
1088
+ if (!contact)
1089
+ throw new Error(`contact not found: ${input.contact_id}`);
1090
+ const beforeRow = stateRow(input.contact_id, input.project_id, db);
1091
+ const before = snapshot(beforeRow);
1092
+ if (before.version !== input.expected_version) {
1093
+ throw new ContactProjectMembershipConflictError(`contact project membership expected_version conflict: expected ${input.expected_version}, current ${before.version}`);
1094
+ }
1095
+ const desiredLinked = direction === "attach";
1096
+ const changed = before.linked !== desiredLinked;
1097
+ const afterRow = {
1098
+ ...beforeRow,
1099
+ linked: desiredLinked ? 1 : 0,
1100
+ revision: changed ? beforeRow.revision + 1 : beforeRow.revision
1101
+ };
1102
+ const changedAt = now();
1103
+ db.run(`INSERT INTO contact_project_membership_states
1104
+ (contact_id, project_id, linked, revision, updated_at)
1105
+ VALUES (?, ?, ?, ?, ?)
1106
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
1107
+ linked = excluded.linked,
1108
+ revision = excluded.revision,
1109
+ updated_at = excluded.updated_at`, [input.contact_id, input.project_id, afterRow.linked, afterRow.revision, changedAt]);
1110
+ if (desiredLinked) {
1111
+ db.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [input.contact_id, input.project_id]);
1112
+ } else {
1113
+ db.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [input.contact_id, input.project_id]);
1114
+ }
1115
+ const after = snapshot(afterRow);
1116
+ const id = receiptId(input);
1117
+ db.run(`INSERT INTO contact_project_membership_receipts (
1118
+ receipt_id, direction, contact_id, project_id, operation_id, step_id,
1119
+ expected_version, before_json, after_json, created_at
1120
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1121
+ id,
1122
+ direction,
1123
+ input.contact_id,
1124
+ input.project_id,
1125
+ input.operation_id,
1126
+ input.step_id,
1127
+ input.expected_version,
1128
+ JSON.stringify(before),
1129
+ JSON.stringify(after),
1130
+ changedAt
1131
+ ]);
1132
+ return {
1133
+ outcome: changed ? "accepted" : "duplicate_of_accepted",
1134
+ operation_id: input.operation_id,
1135
+ step_id: input.step_id,
1136
+ before,
1137
+ after,
1138
+ receipt_id: id
1139
+ };
1140
+ });
1141
+ }
1142
+ var init_project_memberships2 = __esm(() => {
1143
+ init_database();
1144
+ init_project_memberships();
1145
+ });
1146
+
856
1147
  // src/db/contacts.ts
857
1148
  var exports_contacts = {};
858
1149
  __export(exports_contacts, {
@@ -1451,34 +1742,32 @@ function autoLinkContactToCompany(contactId, db) {
1451
1742
  }
1452
1743
  function linkContactToProject(contactId, projectId, db) {
1453
1744
  const d = db || getDatabase();
1454
- d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, projectId]);
1745
+ setContactProjectMembershipWithoutReceipt(contactId, projectId, true, d);
1455
1746
  }
1456
1747
  function unlinkContactFromProject(contactId, projectId, db) {
1457
1748
  const d = db || getDatabase();
1458
- d.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [contactId, projectId]);
1749
+ setContactProjectMembershipWithoutReceipt(contactId, projectId, false, d);
1459
1750
  }
1460
1751
  function getContactProjectIds(contactId, db) {
1461
1752
  const d = db || getDatabase();
1462
- const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id = ?`).all(contactId);
1753
+ const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id = ? ORDER BY project_id ASC`).all(contactId);
1463
1754
  return rows.map((r) => r.project_id);
1464
1755
  }
1465
1756
  function listContactIdsByProject(projectId, db) {
1466
1757
  const d = db || getDatabase();
1467
- const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id = ?`).all(projectId);
1758
+ const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id = ? ORDER BY contact_id ASC`).all(projectId);
1468
1759
  return rows.map((r) => r.contact_id);
1469
1760
  }
1470
1761
  function setContactProjects(contactId, projectIds, db) {
1471
1762
  const d = db || getDatabase();
1472
- d.run(`DELETE FROM contact_projects WHERE contact_id = ?`, [contactId]);
1473
- for (const pid of projectIds) {
1474
- d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, pid]);
1475
- }
1763
+ replaceContactProjectMembershipsWithoutReceipts(contactId, projectIds, d);
1476
1764
  }
1477
1765
  var init_contacts = __esm(() => {
1478
1766
  init_types();
1479
1767
  init_database();
1480
1768
  init_activity();
1481
1769
  init_tombstones();
1770
+ init_project_memberships2();
1482
1771
  });
1483
1772
 
1484
1773
  // src/db/events.ts
@@ -1644,6 +1933,7 @@ function getStorageStatus(db = getDatabase()) {
1644
1933
 
1645
1934
  // src/store/index.ts
1646
1935
  init_contacts();
1936
+ init_project_memberships2();
1647
1937
 
1648
1938
  // src/db/companies.ts
1649
1939
  init_types();
@@ -3173,7 +3463,7 @@ init_database();
3173
3463
  init_database();
3174
3464
  import { existsSync as existsSync3, readFileSync, writeFileSync, mkdirSync as mkdirSync3, unlinkSync } from "fs";
3175
3465
  import { join as join2 } from "path";
3176
- import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
3466
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash as createHash2 } from "crypto";
3177
3467
  var VAULT_DIR = getDataDir();
3178
3468
  var VAULT_CONFIG = join2(VAULT_DIR, "vault.json");
3179
3469
  var VAULT_SESSION = join2(VAULT_DIR, ".vault-session");
@@ -3219,7 +3509,7 @@ function initVault(passphrase) {
3219
3509
  mkdirSync3(DOCUMENTS_DIR, { recursive: true });
3220
3510
  const salt = randomBytes(32);
3221
3511
  const key = deriveKey(passphrase, salt);
3222
- const keyHash = createHash("sha256").update(key).digest("hex");
3512
+ const keyHash = createHash2("sha256").update(key).digest("hex");
3223
3513
  const config = { salt: salt.toString("hex"), key_hash: keyHash, created_at: new Date().toISOString() };
3224
3514
  writeFileSync(VAULT_CONFIG, JSON.stringify(config, null, 2));
3225
3515
  _derivedKey = key;
@@ -3234,7 +3524,7 @@ function unlockVault(passphrase) {
3234
3524
  const config = JSON.parse(readFileSync(VAULT_CONFIG, "utf-8"));
3235
3525
  const salt = Buffer.from(config.salt, "hex");
3236
3526
  const key = deriveKey(passphrase, salt);
3237
- const keyHash = createHash("sha256").update(key).digest("hex");
3527
+ const keyHash = createHash2("sha256").update(key).digest("hex");
3238
3528
  if (keyHash !== config.key_hash)
3239
3529
  return false;
3240
3530
  _derivedKey = key;
@@ -4749,6 +5039,15 @@ class LocalStore {
4749
5039
  async listContactIdsByProject(projectId) {
4750
5040
  return listContactIdsByProject(projectId, this.db);
4751
5041
  }
5042
+ async readContactProjectMembership(contactId, projectId) {
5043
+ return readContactProjectMembership(contactId, projectId, this.db);
5044
+ }
5045
+ async listContactProjectMemberships(projectId, maxItems) {
5046
+ return listContactProjectMemberships(projectId, maxItems, this.db);
5047
+ }
5048
+ async mutateContactProjectMembership(direction, input) {
5049
+ return mutateContactProjectMembership(direction, input, this.db);
5050
+ }
4752
5051
  async createCompany(input) {
4753
5052
  return createCompany(input, this.db);
4754
5053
  }
@@ -5371,20 +5670,33 @@ class ApiStore {
5371
5670
  async findContactByEmailAddress(address) {
5372
5671
  return this.getContactByEmail(address);
5373
5672
  }
5374
- async linkContactToProject() {
5375
- return unavailable("linkContactToProject");
5673
+ async linkContactToProject(contactId, projectId) {
5674
+ await this.client.transport.put(`/contacts/${this.enc(contactId)}/projects/${this.enc(projectId)}`);
5675
+ }
5676
+ async unlinkContactFromProject(contactId, projectId) {
5677
+ await this.del(`/contacts/${this.enc(contactId)}/projects/${this.enc(projectId)}`);
5678
+ }
5679
+ async getContactProjectIds(contactId) {
5680
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/projects`), "project_ids") ?? [];
5376
5681
  }
5377
- async unlinkContactFromProject() {
5378
- return unavailable("unlinkContactFromProject");
5682
+ async setContactProjects(contactId, projectIds) {
5683
+ await this.client.transport.put(`/contacts/${this.enc(contactId)}/projects`, { project_ids: projectIds });
5379
5684
  }
5380
- async getContactProjectIds() {
5381
- return unavailable("getContactProjectIds");
5685
+ async listContactIdsByProject(projectId) {
5686
+ return pick(await this.g(`/projects/${this.enc(projectId)}/contacts`), "contact_ids") ?? [];
5382
5687
  }
5383
- async setContactProjects() {
5384
- return unavailable("setContactProjects");
5688
+ async readContactProjectMembership(contactId, projectId) {
5689
+ return this.client.transport.get(`/projects/${this.enc(projectId)}/contact-memberships/${this.enc(contactId)}`);
5385
5690
  }
5386
- async listContactIdsByProject() {
5387
- return unavailable("listContactIdsByProject");
5691
+ async listContactProjectMemberships(projectId, maxItems) {
5692
+ return this.client.transport.get(`/projects/${this.enc(projectId)}/contact-memberships`, { query: { max_items: maxItems } });
5693
+ }
5694
+ async mutateContactProjectMembership(direction, input) {
5695
+ return this.client.transport.post(`/projects/${this.enc(input.project_id)}/contact-memberships/${this.enc(input.contact_id)}/${direction}`, {
5696
+ operation_id: input.operation_id,
5697
+ step_id: input.step_id,
5698
+ expected_version: input.expected_version
5699
+ });
5388
5700
  }
5389
5701
  async createCompany(input) {
5390
5702
  const res = await this.client.create("companies", stripUndefined(input));
@@ -5907,6 +6219,7 @@ function resetStoreCache() {
5907
6219
  }
5908
6220
 
5909
6221
  // src/index.ts
6222
+ init_project_memberships();
5910
6223
  init_types();
5911
6224
 
5912
6225
  // src/sdk/v1.generated.ts
@@ -6013,6 +6326,34 @@ class ContactsV1Client {
6013
6326
  init
6014
6327
  });
6015
6328
  }
6329
+ async getContactProjectIds(contactId, init) {
6330
+ return this.request("GET", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects`, {
6331
+ body: undefined,
6332
+ query: undefined,
6333
+ init
6334
+ });
6335
+ }
6336
+ async setContactProjects(contactId, body, init) {
6337
+ return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects`, {
6338
+ body,
6339
+ query: undefined,
6340
+ init
6341
+ });
6342
+ }
6343
+ async linkContactToProject(contactId, projectId, init) {
6344
+ return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects/${encodeURIComponent(String(projectId))}`, {
6345
+ body: undefined,
6346
+ query: undefined,
6347
+ init
6348
+ });
6349
+ }
6350
+ async unlinkContactFromProject(contactId, projectId, init) {
6351
+ return this.request("DELETE", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects/${encodeURIComponent(String(projectId))}`, {
6352
+ body: undefined,
6353
+ query: undefined,
6354
+ init
6355
+ });
6356
+ }
6016
6357
  async addTagToContact(contactId, tagId, init) {
6017
6358
  return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/tags/${encodeURIComponent(String(tagId))}`, {
6018
6359
  body: undefined,
@@ -6048,6 +6389,41 @@ class ContactsV1Client {
6048
6389
  init
6049
6390
  });
6050
6391
  }
6392
+ async listContactProjectMemberships(projectId, query, init) {
6393
+ return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships`, {
6394
+ body: undefined,
6395
+ query,
6396
+ init
6397
+ });
6398
+ }
6399
+ async readContactProjectMembership(projectId, contactId, init) {
6400
+ return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}`, {
6401
+ body: undefined,
6402
+ query: undefined,
6403
+ init
6404
+ });
6405
+ }
6406
+ async attachContactProjectMembership(projectId, contactId, body, init) {
6407
+ return this.request("POST", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}/attach`, {
6408
+ body,
6409
+ query: undefined,
6410
+ init
6411
+ });
6412
+ }
6413
+ async detachContactProjectMembership(projectId, contactId, body, init) {
6414
+ return this.request("POST", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}/detach`, {
6415
+ body,
6416
+ query: undefined,
6417
+ init
6418
+ });
6419
+ }
6420
+ async listContactIdsByProject(projectId, init) {
6421
+ return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contacts`, {
6422
+ body: undefined,
6423
+ query: undefined,
6424
+ init
6425
+ });
6426
+ }
6051
6427
  async getStats(init) {
6052
6428
  return this.request("GET", `/v1/stats`, {
6053
6429
  body: undefined,
@@ -6123,6 +6499,7 @@ function getPackageVersion() {
6123
6499
  // src/server/openapi.ts
6124
6500
  var contactSchema = {
6125
6501
  type: "object",
6502
+ required: ["tags"],
6126
6503
  properties: {
6127
6504
  id: { type: "string" },
6128
6505
  first_name: { type: "string" },
@@ -6137,6 +6514,7 @@ var contactSchema = {
6137
6514
  sensitivity: { type: "string" },
6138
6515
  archived: { type: "boolean" },
6139
6516
  priority: { type: "number" },
6517
+ tags: { type: "array", items: { $ref: "#/components/schemas/Tag" } },
6140
6518
  created_at: { type: "string" },
6141
6519
  updated_at: { type: "string" }
6142
6520
  }
@@ -6256,6 +6634,58 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
6256
6634
  color: { type: "string" },
6257
6635
  description: { type: "string" }
6258
6636
  }
6637
+ },
6638
+ ProjectIdsInput: {
6639
+ type: "object",
6640
+ required: ["project_ids"],
6641
+ properties: {
6642
+ project_ids: {
6643
+ type: "array",
6644
+ items: { type: "string", minLength: 1 },
6645
+ uniqueItems: true
6646
+ }
6647
+ }
6648
+ },
6649
+ ContactProjectMembershipSnapshot: {
6650
+ type: "object",
6651
+ required: ["contact_id", "project_id", "linked", "version"],
6652
+ properties: {
6653
+ contact_id: { type: "string" },
6654
+ project_id: { type: "string" },
6655
+ linked: { type: "boolean" },
6656
+ version: { type: "string" }
6657
+ }
6658
+ },
6659
+ ContactProjectMembershipMutationInput: {
6660
+ type: "object",
6661
+ required: ["operation_id", "step_id", "expected_version"],
6662
+ properties: {
6663
+ operation_id: { type: "string", minLength: 1 },
6664
+ step_id: { type: "string", minLength: 1 },
6665
+ expected_version: { type: "string", minLength: 1 }
6666
+ }
6667
+ },
6668
+ ContactProjectMembershipMutationResult: {
6669
+ type: "object",
6670
+ required: ["outcome", "operation_id", "step_id", "before", "after", "receipt_id"],
6671
+ properties: {
6672
+ outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
6673
+ operation_id: { type: "string" },
6674
+ step_id: { type: "string" },
6675
+ before: { $ref: "#/components/schemas/ContactProjectMembershipSnapshot" },
6676
+ after: { $ref: "#/components/schemas/ContactProjectMembershipSnapshot" },
6677
+ receipt_id: { type: "string" }
6678
+ }
6679
+ },
6680
+ ContactProjectMembershipListResult: {
6681
+ type: "object",
6682
+ required: ["project_id", "contact_ids", "complete", "membership_revision"],
6683
+ properties: {
6684
+ project_id: { type: "string" },
6685
+ contact_ids: { type: "array", items: { type: "string" } },
6686
+ complete: { type: "boolean", const: true },
6687
+ membership_revision: { type: "string" }
6688
+ }
6259
6689
  }
6260
6690
  }
6261
6691
  },
@@ -6269,6 +6699,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
6269
6699
  { name: "q", in: "query", schema: { type: "string" } },
6270
6700
  { name: "company_id", in: "query", schema: { type: "string" } },
6271
6701
  { name: "status", in: "query", schema: { type: "string" } },
6702
+ { name: "tag_id", in: "query", schema: { type: "string" } },
6272
6703
  { name: "limit", in: "query", schema: { type: "number" } },
6273
6704
  { name: "offset", in: "query", schema: { type: "number" } }
6274
6705
  ],
@@ -6455,6 +6886,167 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
6455
6886
  })
6456
6887
  }
6457
6888
  },
6889
+ "/v1/contacts/{contact_id}/projects": {
6890
+ get: {
6891
+ operationId: "getContactProjectIds",
6892
+ summary: "List project ids attached to a contact",
6893
+ parameters: [
6894
+ { name: "contact_id", in: "path", required: true, schema: { type: "string" } }
6895
+ ],
6896
+ responses: objResponse({
6897
+ contact_id: { type: "string" },
6898
+ project_ids: { type: "array", items: { type: "string" } }
6899
+ })
6900
+ },
6901
+ put: {
6902
+ operationId: "setContactProjects",
6903
+ summary: "Atomically replace a contact's project memberships",
6904
+ parameters: [
6905
+ { name: "contact_id", in: "path", required: true, schema: { type: "string" } }
6906
+ ],
6907
+ requestBody: {
6908
+ required: true,
6909
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectIdsInput" } } }
6910
+ },
6911
+ responses: objResponse({
6912
+ contact_id: { type: "string" },
6913
+ project_ids: { type: "array", items: { type: "string" } }
6914
+ })
6915
+ }
6916
+ },
6917
+ "/v1/contacts/{contact_id}/projects/{project_id}": {
6918
+ put: {
6919
+ operationId: "linkContactToProject",
6920
+ summary: "Attach a contact to a project idempotently",
6921
+ parameters: [
6922
+ { name: "contact_id", in: "path", required: true, schema: { type: "string" } },
6923
+ { name: "project_id", in: "path", required: true, schema: { type: "string" } }
6924
+ ],
6925
+ responses: objResponse({
6926
+ attached: { type: "boolean" },
6927
+ contact_id: { type: "string" },
6928
+ project_id: { type: "string" }
6929
+ })
6930
+ },
6931
+ delete: {
6932
+ operationId: "unlinkContactFromProject",
6933
+ summary: "Detach a contact from a project",
6934
+ parameters: [
6935
+ { name: "contact_id", in: "path", required: true, schema: { type: "string" } },
6936
+ { name: "project_id", in: "path", required: true, schema: { type: "string" } }
6937
+ ],
6938
+ responses: objResponse({
6939
+ removed: { type: "boolean" },
6940
+ contact_id: { type: "string" },
6941
+ project_id: { type: "string" }
6942
+ })
6943
+ }
6944
+ },
6945
+ "/v1/projects/{project_id}/contacts": {
6946
+ get: {
6947
+ operationId: "listContactIdsByProject",
6948
+ summary: "List contact ids attached to a project",
6949
+ parameters: [
6950
+ { name: "project_id", in: "path", required: true, schema: { type: "string" } }
6951
+ ],
6952
+ responses: objResponse({
6953
+ project_id: { type: "string" },
6954
+ contact_ids: { type: "array", items: { type: "string" } }
6955
+ })
6956
+ }
6957
+ },
6958
+ "/v1/projects/{project_id}/contact-memberships": {
6959
+ get: {
6960
+ operationId: "listContactProjectMemberships",
6961
+ summary: "List the complete authoritative contact membership collection for a project",
6962
+ parameters: [
6963
+ { name: "project_id", in: "path", required: true, schema: { type: "string" } },
6964
+ { name: "max_items", in: "query", required: true, schema: { type: "integer", minimum: 1 } }
6965
+ ],
6966
+ responses: {
6967
+ "200": {
6968
+ content: {
6969
+ "application/json": {
6970
+ schema: { $ref: "#/components/schemas/ContactProjectMembershipListResult" }
6971
+ }
6972
+ }
6973
+ }
6974
+ }
6975
+ }
6976
+ },
6977
+ "/v1/projects/{project_id}/contact-memberships/{contact_id}": {
6978
+ get: {
6979
+ operationId: "readContactProjectMembership",
6980
+ summary: "Read one authoritative contact-project membership snapshot",
6981
+ parameters: [
6982
+ { name: "project_id", in: "path", required: true, schema: { type: "string" } },
6983
+ { name: "contact_id", in: "path", required: true, schema: { type: "string" } }
6984
+ ],
6985
+ responses: {
6986
+ "200": {
6987
+ content: {
6988
+ "application/json": {
6989
+ schema: { $ref: "#/components/schemas/ContactProjectMembershipSnapshot" }
6990
+ }
6991
+ }
6992
+ }
6993
+ }
6994
+ }
6995
+ },
6996
+ "/v1/projects/{project_id}/contact-memberships/{contact_id}/attach": {
6997
+ post: {
6998
+ operationId: "attachContactProjectMembership",
6999
+ summary: "Attach a contact to a project under expected-version CAS with a replay-safe receipt",
7000
+ parameters: [
7001
+ { name: "project_id", in: "path", required: true, schema: { type: "string" } },
7002
+ { name: "contact_id", in: "path", required: true, schema: { type: "string" } }
7003
+ ],
7004
+ requestBody: {
7005
+ required: true,
7006
+ content: {
7007
+ "application/json": {
7008
+ schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationInput" }
7009
+ }
7010
+ }
7011
+ },
7012
+ responses: {
7013
+ "200": {
7014
+ content: {
7015
+ "application/json": {
7016
+ schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationResult" }
7017
+ }
7018
+ }
7019
+ }
7020
+ }
7021
+ }
7022
+ },
7023
+ "/v1/projects/{project_id}/contact-memberships/{contact_id}/detach": {
7024
+ post: {
7025
+ operationId: "detachContactProjectMembership",
7026
+ summary: "Detach a contact from a project under expected-version CAS with a replay-safe receipt",
7027
+ parameters: [
7028
+ { name: "project_id", in: "path", required: true, schema: { type: "string" } },
7029
+ { name: "contact_id", in: "path", required: true, schema: { type: "string" } }
7030
+ ],
7031
+ requestBody: {
7032
+ required: true,
7033
+ content: {
7034
+ "application/json": {
7035
+ schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationInput" }
7036
+ }
7037
+ }
7038
+ },
7039
+ responses: {
7040
+ "200": {
7041
+ content: {
7042
+ "application/json": {
7043
+ schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationResult" }
7044
+ }
7045
+ }
7046
+ }
7047
+ }
7048
+ }
7049
+ },
6458
7050
  "/v1/stats": {
6459
7051
  get: {
6460
7052
  operationId: "getStats",
@@ -6479,6 +7071,7 @@ export {
6479
7071
  DuplicateAudienceIdError,
6480
7072
  ContactsV1Client,
6481
7073
  ApiError as ContactsV1ApiError,
7074
+ ContactProjectMembershipConflictError,
6482
7075
  ContactNotFoundError,
6483
7076
  CompanyNotFoundError,
6484
7077
  CONSENT_STATUSES,