@hasna/contacts 0.6.34 → 0.6.36
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/README.md +10 -1
- package/dashboard/dist/assets/index-0l6aQb1t.css +1 -0
- package/dashboard/dist/assets/index-opnZdkVD.js +229 -0
- package/dashboard/dist/index.html +13 -0
- package/dist/cli/commands/advanced.d.ts +3 -0
- package/dist/cli/commands/advanced.d.ts.map +1 -0
- package/dist/cli/commands/audience.d.ts +18 -0
- package/dist/cli/commands/audience.d.ts.map +1 -0
- package/dist/cli/commands/core.d.ts +3 -0
- package/dist/cli/commands/core.d.ts.map +1 -0
- package/dist/cli/commands/crm.d.ts +3 -0
- package/dist/cli/commands/crm.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2994 -579
- package/dist/cli/storage.d.ts +3 -0
- package/dist/cli/storage.d.ts.map +1 -0
- package/dist/cli/utils.d.ts +10 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cloud/http-storage.d.ts +88 -0
- package/dist/cloud/http-storage.d.ts.map +1 -0
- package/dist/db/activity.d.ts +15 -0
- package/dist/db/activity.d.ts.map +1 -0
- package/dist/db/applications.d.ts +10 -0
- package/dist/db/applications.d.ts.map +1 -0
- package/dist/db/audiences.d.ts +51 -0
- package/dist/db/audiences.d.ts.map +1 -0
- package/dist/db/companies.d.ts +19 -0
- package/dist/db/companies.d.ts.map +1 -0
- package/dist/db/contact-tasks.d.ts +19 -0
- package/dist/db/contact-tasks.d.ts.map +1 -0
- package/dist/db/contacts.d.ts +30 -0
- package/dist/db/contacts.d.ts.map +1 -0
- package/dist/db/coordination.d.ts +30 -0
- package/dist/db/coordination.d.ts.map +1 -0
- package/dist/db/database.d.ts +8 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/deals.d.ts +14 -0
- package/dist/db/deals.d.ts.map +1 -0
- package/dist/db/documents.d.ts +38 -0
- package/dist/db/documents.d.ts.map +1 -0
- package/dist/db/events.d.ts +14 -0
- package/dist/db/events.d.ts.map +1 -0
- package/dist/db/field-history.d.ts +17 -0
- package/dist/db/field-history.d.ts.map +1 -0
- package/dist/db/freshness.d.ts +24 -0
- package/dist/db/freshness.d.ts.map +1 -0
- package/dist/db/graph.d.ts +21 -0
- package/dist/db/graph.d.ts.map +1 -0
- package/dist/db/groups.d.ts +22 -0
- package/dist/db/groups.d.ts.map +1 -0
- package/dist/db/health.d.ts +40 -0
- package/dist/db/health.d.ts.map +1 -0
- package/dist/db/identity.d.ts +32 -0
- package/dist/db/identity.d.ts.map +1 -0
- package/dist/db/job-history.d.ts +29 -0
- package/dist/db/job-history.d.ts.map +1 -0
- package/dist/db/learnings.d.ts +43 -0
- package/dist/db/learnings.d.ts.map +1 -0
- package/dist/db/notes.d.ts +8 -0
- package/dist/db/notes.d.ts.map +1 -0
- package/dist/db/org-chart.d.ts +37 -0
- package/dist/db/org-chart.d.ts.map +1 -0
- package/dist/db/org-members.d.ts +9 -0
- package/dist/db/org-members.d.ts.map +1 -0
- package/dist/db/paths.d.ts +15 -0
- package/dist/db/paths.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts +8 -0
- package/dist/db/pg-migrations.d.ts.map +1 -0
- package/dist/db/project-memberships.d.ts +8 -0
- package/dist/db/project-memberships.d.ts.map +1 -0
- package/dist/db/relationships.d.ts +30 -0
- package/dist/db/relationships.d.ts.map +1 -0
- package/dist/db/signals.d.ts +18 -0
- package/dist/db/signals.d.ts.map +1 -0
- package/dist/db/sqlite-adapter.d.ts +15 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/storage.d.ts +15 -0
- package/dist/db/storage.d.ts.map +1 -0
- package/dist/db/tags.d.ts +14 -0
- package/dist/db/tags.d.ts.map +1 -0
- package/dist/db/tombstones.d.ts +19 -0
- package/dist/db/tombstones.d.ts.map +1 -0
- package/dist/db/vendor-comms.d.ts +15 -0
- package/dist/db/vendor-comms.d.ts.map +1 -0
- package/dist/generated/storage-kit/health.d.ts +20 -0
- package/dist/generated/storage-kit/health.d.ts.map +1 -0
- package/dist/generated/storage-kit/index.d.ts +8 -0
- package/dist/generated/storage-kit/index.d.ts.map +1 -0
- package/dist/generated/storage-kit/migrations.d.ts +48 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -0
- package/dist/generated/storage-kit/mode.d.ts +48 -0
- package/dist/generated/storage-kit/mode.d.ts.map +1 -0
- package/dist/generated/storage-kit/pool.d.ts +34 -0
- package/dist/generated/storage-kit/pool.d.ts.map +1 -0
- package/dist/generated/storage-kit/query.d.ts +36 -0
- package/dist/generated/storage-kit/query.d.ts.map +1 -0
- package/dist/generated/storage-kit/tls.d.ts +26 -0
- package/dist/generated/storage-kit/tls.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +805 -75
- package/dist/lib/apple-contacts.d.ts +3 -0
- package/dist/lib/apple-contacts.d.ts.map +1 -0
- package/dist/lib/audience-contract.d.ts +215 -0
- package/dist/lib/audience-contract.d.ts.map +1 -0
- package/dist/lib/audit.d.ts +12 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/brief.d.ts +3 -0
- package/dist/lib/brief.d.ts.map +1 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/connector.d.ts +44 -0
- package/dist/lib/connector.d.ts.map +1 -0
- package/dist/lib/context.d.ts +5 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/dedup.d.ts +10 -0
- package/dist/lib/dedup.d.ts.map +1 -0
- package/dist/lib/document-scanner.d.ts +8 -0
- package/dist/lib/document-scanner.d.ts.map +1 -0
- package/dist/lib/embeddings.d.ts +9 -0
- package/dist/lib/embeddings.d.ts.map +1 -0
- package/dist/lib/export.d.ts +8 -0
- package/dist/lib/export.d.ts.map +1 -0
- package/dist/lib/freshness.d.ts +19 -0
- package/dist/lib/freshness.d.ts.map +1 -0
- package/dist/lib/gmail-import.d.ts +51 -0
- package/dist/lib/gmail-import.d.ts.map +1 -0
- package/dist/lib/google-contacts.d.ts +115 -0
- package/dist/lib/google-contacts.d.ts.map +1 -0
- package/dist/lib/images.d.ts +30 -0
- package/dist/lib/images.d.ts.map +1 -0
- package/dist/lib/import.d.ts +10 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/lib/learning-maintenance.d.ts +8 -0
- package/dist/lib/learning-maintenance.d.ts.map +1 -0
- package/dist/lib/mailery-sync.d.ts +46 -0
- package/dist/lib/mailery-sync.d.ts.map +1 -0
- package/dist/lib/meeting-capture.d.ts +15 -0
- package/dist/lib/meeting-capture.d.ts.map +1 -0
- package/dist/lib/package-version.d.ts +6 -0
- package/dist/lib/package-version.d.ts.map +1 -0
- package/dist/lib/signals.d.ts +9 -0
- package/dist/lib/signals.d.ts.map +1 -0
- package/dist/lib/signature-parser.d.ts +36 -0
- package/dist/lib/signature-parser.d.ts.map +1 -0
- package/dist/lib/stats.d.ts +24 -0
- package/dist/lib/stats.d.ts.map +1 -0
- package/dist/lib/timeline.d.ts +11 -0
- package/dist/lib/timeline.d.ts.map +1 -0
- package/dist/lib/upcoming.d.ts +14 -0
- package/dist/lib/upcoming.d.ts.map +1 -0
- package/dist/lib/vault.d.ts +28 -0
- package/dist/lib/vault.d.ts.map +1 -0
- package/dist/mcp/handlers/advanced.d.ts +14 -0
- package/dist/mcp/handlers/advanced.d.ts.map +1 -0
- package/dist/mcp/handlers/audience.d.ts +9 -0
- package/dist/mcp/handlers/audience.d.ts.map +1 -0
- package/dist/mcp/handlers/core.d.ts +10 -0
- package/dist/mcp/handlers/core.d.ts.map +1 -0
- package/dist/mcp/handlers/crm.d.ts +14 -0
- package/dist/mcp/handlers/crm.d.ts.map +1 -0
- package/dist/mcp/handlers/index.d.ts +4 -0
- package/dist/mcp/handlers/index.d.ts.map +1 -0
- package/dist/mcp/handlers/types.d.ts +10 -0
- package/dist/mcp/handlers/types.d.ts.map +1 -0
- package/dist/mcp/http.d.ts +17 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +1054 -291
- package/dist/mcp/register-tools.d.ts +3 -0
- package/dist/mcp/register-tools.d.ts.map +1 -0
- package/dist/mcp/schema.d.ts +3 -0
- package/dist/mcp/schema.d.ts.map +1 -0
- package/dist/mcp/storage-tools.d.ts +3 -0
- package/dist/mcp/storage-tools.d.ts.map +1 -0
- package/dist/mcp/tools.d.ts +3799 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +16 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +63 -0
- package/dist/sdk/v1.generated.d.ts +270 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -0
- package/dist/server/cloud-index.d.ts +3 -0
- package/dist/server/cloud-index.d.ts.map +1 -0
- package/dist/server/cloud-serve.d.ts +3 -0
- package/dist/server/cloud-serve.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +53 -0
- package/dist/server/cloud.d.ts.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1750 -332
- package/dist/server/openapi.d.ts +1134 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/pg-store.d.ts +893 -0
- package/dist/server/pg-store.d.ts.map +1 -0
- package/dist/server/security.d.ts +24 -0
- package/dist/server/security.d.ts.map +1 -0
- package/dist/server/serve.d.ts +9 -0
- package/dist/server/serve.d.ts.map +1 -0
- package/dist/server/v1.d.ts +28 -0
- package/dist/server/v1.d.ts.map +1 -0
- package/dist/store/index.d.ts +387 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +856 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/project-memberships.d.ts +32 -0
- package/dist/types/project-memberships.d.ts.map +1 -0
- package/hasna.contract.json +1 -1
- package/migrations/0015_contact_project_membership_authority.sql +88 -0
- package/package.json +4 -2
- package/vendor/fast-uri/README.md +12 -0
- package/dist/server/cloud-index.js +0 -9187
package/dist/index.js
CHANGED
|
@@ -55,43 +55,169 @@ class SqliteAdapter {
|
|
|
55
55
|
}
|
|
56
56
|
var init_sqlite_adapter = () => {};
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
import {
|
|
58
|
+
// node_modules/.pnpm/@hasna+paths@0.2.2/node_modules/@hasna/paths/dist/index.js
|
|
59
|
+
import { homedir } from "os";
|
|
60
60
|
import { join } from "path";
|
|
61
|
+
function assertApp(app) {
|
|
62
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
63
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
64
|
+
}
|
|
65
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
66
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function assertKind(kind) {
|
|
70
|
+
if (!PATH_KINDS.includes(kind)) {
|
|
71
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${PATH_KINDS.join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function envOf(options) {
|
|
75
|
+
return options.env ?? process.env;
|
|
76
|
+
}
|
|
77
|
+
function envValue(options, kind) {
|
|
78
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
79
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
function isMacOS(platform) {
|
|
82
|
+
return platform === "darwin";
|
|
83
|
+
}
|
|
84
|
+
function baseDir(kind, options) {
|
|
85
|
+
assertKind(kind);
|
|
86
|
+
const override = envValue(options, kind);
|
|
87
|
+
if (override)
|
|
88
|
+
return override;
|
|
89
|
+
const home = options.home ?? homedir();
|
|
90
|
+
const platform = options.platform ?? process.platform;
|
|
91
|
+
if (isMacOS(platform)) {
|
|
92
|
+
switch (kind) {
|
|
93
|
+
case "config":
|
|
94
|
+
case "data":
|
|
95
|
+
return join(home, "Library", "Application Support", "Hasna");
|
|
96
|
+
case "cache":
|
|
97
|
+
return join(home, "Library", "Caches", "Hasna");
|
|
98
|
+
case "state":
|
|
99
|
+
return join(home, "Library", "Logs", "Hasna");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
switch (kind) {
|
|
103
|
+
case "config":
|
|
104
|
+
return join(home, ".config", "hasna");
|
|
105
|
+
case "data":
|
|
106
|
+
return join(home, ".local", "share", "hasna");
|
|
107
|
+
case "state":
|
|
108
|
+
return join(home, ".local", "state", "hasna");
|
|
109
|
+
case "cache":
|
|
110
|
+
return join(home, ".cache", "hasna");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function resolvePath(kind, options) {
|
|
114
|
+
assertKind(kind);
|
|
115
|
+
assertApp(options.app);
|
|
116
|
+
const appSegment = options.internal === true ? join("internal", options.app) : options.app;
|
|
117
|
+
return join(baseDir(kind, options), appSegment);
|
|
118
|
+
}
|
|
119
|
+
function dataDir(options) {
|
|
120
|
+
return resolvePath("data", options);
|
|
121
|
+
}
|
|
122
|
+
function stateDir(options) {
|
|
123
|
+
return resolvePath("state", options);
|
|
124
|
+
}
|
|
125
|
+
var PATH_KINDS, KIND_ENV, APP_SLUG_RE;
|
|
126
|
+
var init_dist = __esm(() => {
|
|
127
|
+
PATH_KINDS = ["config", "data", "state", "cache"];
|
|
128
|
+
KIND_ENV = {
|
|
129
|
+
config: "HASNA_CONFIG_HOME",
|
|
130
|
+
data: "HASNA_DATA_HOME",
|
|
131
|
+
state: "HASNA_STATE_HOME",
|
|
132
|
+
cache: "HASNA_CACHE_HOME"
|
|
133
|
+
};
|
|
134
|
+
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// src/db/paths.ts
|
|
138
|
+
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
139
|
+
import { homedir as homedir2 } from "os";
|
|
140
|
+
import { join as join2 } from "path";
|
|
61
141
|
function ensurePrivateDir(dir) {
|
|
62
142
|
if (!existsSync(dir))
|
|
63
143
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
64
144
|
chmodSync(dir, 448);
|
|
65
145
|
}
|
|
146
|
+
function home() {
|
|
147
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
148
|
+
}
|
|
149
|
+
function hasContent(dir) {
|
|
150
|
+
if (!existsSync(dir))
|
|
151
|
+
return false;
|
|
152
|
+
try {
|
|
153
|
+
return readdirSync(dir).length > 0;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function adoptLegacy(source, target) {
|
|
159
|
+
if (!hasContent(source) || hasContent(target))
|
|
160
|
+
return;
|
|
161
|
+
ensurePrivateDir(target);
|
|
162
|
+
for (const entry of readdirSync(source)) {
|
|
163
|
+
if (entry === ".vault-session")
|
|
164
|
+
continue;
|
|
165
|
+
if (entry === "{backups,images,documents}")
|
|
166
|
+
continue;
|
|
167
|
+
const oldPath = join2(source, entry);
|
|
168
|
+
const newPath = join2(target, entry);
|
|
169
|
+
const st = statSync(oldPath);
|
|
170
|
+
if (st.isDirectory()) {
|
|
171
|
+
cpSync(oldPath, newPath, { recursive: true });
|
|
172
|
+
chmodSync(newPath, 448);
|
|
173
|
+
} else if (st.isFile()) {
|
|
174
|
+
copyFileSync(oldPath, newPath);
|
|
175
|
+
chmodSync(newPath, 384);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
66
179
|
function getDataDir() {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
180
|
+
const base = home();
|
|
181
|
+
const target = dataDir({ app: "contacts", home: base });
|
|
182
|
+
if (!checkedDataTargets.has(target)) {
|
|
183
|
+
adoptLegacy(join2(base, ".hasna", "contacts"), target);
|
|
184
|
+
adoptLegacy(join2(base, ".contacts"), target);
|
|
185
|
+
checkedDataTargets.add(target);
|
|
186
|
+
}
|
|
187
|
+
ensurePrivateDir(target);
|
|
188
|
+
return target;
|
|
189
|
+
}
|
|
190
|
+
function getStateDir() {
|
|
191
|
+
const base = home();
|
|
192
|
+
const target = stateDir({ app: "contacts", home: base });
|
|
193
|
+
if (!checkedStateTargets.has(target)) {
|
|
194
|
+
if (!hasContent(target)) {
|
|
195
|
+
const legacySession = join2(base, ".hasna", "contacts", ".vault-session");
|
|
196
|
+
if (existsSync(legacySession)) {
|
|
197
|
+
ensurePrivateDir(target);
|
|
198
|
+
const targetSession = join2(target, ".vault-session");
|
|
199
|
+
copyFileSync(legacySession, targetSession);
|
|
200
|
+
chmodSync(targetSession, 384);
|
|
80
201
|
}
|
|
81
202
|
}
|
|
203
|
+
checkedStateTargets.add(target);
|
|
82
204
|
}
|
|
83
|
-
ensurePrivateDir(
|
|
84
|
-
|
|
85
|
-
return newDir;
|
|
205
|
+
ensurePrivateDir(target);
|
|
206
|
+
return target;
|
|
86
207
|
}
|
|
87
208
|
function getDbPath() {
|
|
88
209
|
if (process.env["HASNA_CONTACTS_DB_PATH"])
|
|
89
210
|
return process.env["HASNA_CONTACTS_DB_PATH"];
|
|
90
211
|
if (process.env["CONTACTS_DB_PATH"])
|
|
91
212
|
return process.env["CONTACTS_DB_PATH"];
|
|
92
|
-
return
|
|
213
|
+
return join2(getDataDir(), "contacts.db");
|
|
93
214
|
}
|
|
94
|
-
var
|
|
215
|
+
var checkedDataTargets, checkedStateTargets;
|
|
216
|
+
var init_paths = __esm(() => {
|
|
217
|
+
init_dist();
|
|
218
|
+
checkedDataTargets = new Set;
|
|
219
|
+
checkedStateTargets = new Set;
|
|
220
|
+
});
|
|
95
221
|
|
|
96
222
|
// src/db/database.ts
|
|
97
223
|
var exports_database = {};
|
|
@@ -99,6 +225,7 @@ __export(exports_database, {
|
|
|
99
225
|
uuid: () => uuid,
|
|
100
226
|
resetDatabase: () => resetDatabase,
|
|
101
227
|
now: () => now,
|
|
228
|
+
getStateDir: () => getStateDir,
|
|
102
229
|
getDbPath: () => getDbPath,
|
|
103
230
|
getDatabase: () => getDatabase,
|
|
104
231
|
getDataDir: () => getDataDir
|
|
@@ -144,7 +271,10 @@ function uuid() {
|
|
|
144
271
|
return crypto.randomUUID();
|
|
145
272
|
}
|
|
146
273
|
function now() {
|
|
147
|
-
|
|
274
|
+
const currentMs = Date.now();
|
|
275
|
+
const nextMs = currentMs > lastNowMs ? currentMs : lastNowMs + 1;
|
|
276
|
+
lastNowMs = nextMs;
|
|
277
|
+
return new Date(nextMs).toISOString();
|
|
148
278
|
}
|
|
149
279
|
function quoteIdentifier(identifier) {
|
|
150
280
|
return `"${identifier.replaceAll('"', '""')}"`;
|
|
@@ -182,7 +312,7 @@ function runMigrations(db) {
|
|
|
182
312
|
db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${i})`);
|
|
183
313
|
}
|
|
184
314
|
}
|
|
185
|
-
var MIGRATIONS, _db = null;
|
|
315
|
+
var MIGRATIONS, _db = null, lastNowMs = 0;
|
|
186
316
|
var init_database = __esm(() => {
|
|
187
317
|
init_sqlite_adapter();
|
|
188
318
|
init_paths();
|
|
@@ -754,6 +884,81 @@ var init_database = __esm(() => {
|
|
|
754
884
|
);
|
|
755
885
|
|
|
756
886
|
CREATE INDEX IF NOT EXISTS idx_contacts_tombstones_deleted_at ON _contacts_tombstones(deleted_at);
|
|
887
|
+
`,
|
|
888
|
+
`
|
|
889
|
+
CREATE TABLE IF NOT EXISTS contact_project_membership_states (
|
|
890
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
891
|
+
project_id TEXT NOT NULL,
|
|
892
|
+
linked INTEGER NOT NULL CHECK(linked IN (0, 1)),
|
|
893
|
+
revision INTEGER NOT NULL DEFAULT 0 CHECK(revision >= 0),
|
|
894
|
+
updated_at TEXT NOT NULL,
|
|
895
|
+
PRIMARY KEY (contact_id, project_id)
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
INSERT OR IGNORE INTO contact_project_membership_states
|
|
899
|
+
(contact_id, project_id, linked, revision, updated_at)
|
|
900
|
+
SELECT contact_id, project_id, 1, 0, datetime('now')
|
|
901
|
+
FROM contact_projects;
|
|
902
|
+
|
|
903
|
+
CREATE TABLE IF NOT EXISTS contact_project_membership_receipts (
|
|
904
|
+
receipt_id TEXT PRIMARY KEY,
|
|
905
|
+
direction TEXT NOT NULL CHECK(direction IN ('attach', 'detach')),
|
|
906
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
907
|
+
project_id TEXT NOT NULL,
|
|
908
|
+
operation_id TEXT NOT NULL,
|
|
909
|
+
step_id TEXT NOT NULL,
|
|
910
|
+
expected_version TEXT NOT NULL,
|
|
911
|
+
before_json TEXT NOT NULL,
|
|
912
|
+
after_json TEXT NOT NULL,
|
|
913
|
+
created_at TEXT NOT NULL,
|
|
914
|
+
UNIQUE(operation_id, step_id)
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
CREATE INDEX IF NOT EXISTS idx_contact_project_membership_states_project
|
|
918
|
+
ON contact_project_membership_states(project_id, linked, contact_id);
|
|
919
|
+
CREATE INDEX IF NOT EXISTS idx_contact_project_membership_receipts_target
|
|
920
|
+
ON contact_project_membership_receipts(contact_id, project_id, created_at);
|
|
921
|
+
|
|
922
|
+
CREATE TRIGGER IF NOT EXISTS sync_contact_project_membership_state_after_insert
|
|
923
|
+
AFTER INSERT ON contact_projects
|
|
924
|
+
BEGIN
|
|
925
|
+
INSERT INTO contact_project_membership_states
|
|
926
|
+
(contact_id, project_id, linked, revision, updated_at)
|
|
927
|
+
VALUES (NEW.contact_id, NEW.project_id, 1, 1, datetime('now'))
|
|
928
|
+
ON CONFLICT(contact_id, project_id) DO UPDATE SET
|
|
929
|
+
linked = 1,
|
|
930
|
+
revision = CASE
|
|
931
|
+
WHEN contact_project_membership_states.linked = 1
|
|
932
|
+
THEN contact_project_membership_states.revision
|
|
933
|
+
ELSE contact_project_membership_states.revision + 1
|
|
934
|
+
END,
|
|
935
|
+
updated_at = CASE
|
|
936
|
+
WHEN contact_project_membership_states.linked = 1
|
|
937
|
+
THEN contact_project_membership_states.updated_at
|
|
938
|
+
ELSE datetime('now')
|
|
939
|
+
END;
|
|
940
|
+
END;
|
|
941
|
+
|
|
942
|
+
CREATE TRIGGER IF NOT EXISTS sync_contact_project_membership_state_after_delete
|
|
943
|
+
AFTER DELETE ON contact_projects
|
|
944
|
+
WHEN EXISTS (SELECT 1 FROM contacts WHERE id = OLD.contact_id)
|
|
945
|
+
BEGIN
|
|
946
|
+
INSERT INTO contact_project_membership_states
|
|
947
|
+
(contact_id, project_id, linked, revision, updated_at)
|
|
948
|
+
VALUES (OLD.contact_id, OLD.project_id, 0, 1, datetime('now'))
|
|
949
|
+
ON CONFLICT(contact_id, project_id) DO UPDATE SET
|
|
950
|
+
linked = 0,
|
|
951
|
+
revision = CASE
|
|
952
|
+
WHEN contact_project_membership_states.linked = 0
|
|
953
|
+
THEN contact_project_membership_states.revision
|
|
954
|
+
ELSE contact_project_membership_states.revision + 1
|
|
955
|
+
END,
|
|
956
|
+
updated_at = CASE
|
|
957
|
+
WHEN contact_project_membership_states.linked = 0
|
|
958
|
+
THEN contact_project_membership_states.updated_at
|
|
959
|
+
ELSE datetime('now')
|
|
960
|
+
END;
|
|
961
|
+
END;
|
|
757
962
|
`
|
|
758
963
|
];
|
|
759
964
|
});
|
|
@@ -853,6 +1058,219 @@ var init_tombstones = __esm(() => {
|
|
|
853
1058
|
init_database();
|
|
854
1059
|
});
|
|
855
1060
|
|
|
1061
|
+
// src/types/project-memberships.ts
|
|
1062
|
+
var ContactProjectMembershipConflictError;
|
|
1063
|
+
var init_project_memberships = __esm(() => {
|
|
1064
|
+
ContactProjectMembershipConflictError = class ContactProjectMembershipConflictError extends Error {
|
|
1065
|
+
constructor(message) {
|
|
1066
|
+
super(message);
|
|
1067
|
+
this.name = "ContactProjectMembershipConflictError";
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// src/db/project-memberships.ts
|
|
1073
|
+
import { createHash } from "crypto";
|
|
1074
|
+
function digest(value) {
|
|
1075
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1076
|
+
}
|
|
1077
|
+
function membershipVersion(contactId, projectId, linked, revision) {
|
|
1078
|
+
return `cpmv_${digest(JSON.stringify([contactId, projectId, linked, revision])).slice(0, 32)}`;
|
|
1079
|
+
}
|
|
1080
|
+
function receiptId(input) {
|
|
1081
|
+
return `cpmr_${digest(JSON.stringify([
|
|
1082
|
+
input.operation_id,
|
|
1083
|
+
input.step_id,
|
|
1084
|
+
input.contact_id,
|
|
1085
|
+
input.project_id
|
|
1086
|
+
])).slice(0, 32)}`;
|
|
1087
|
+
}
|
|
1088
|
+
function stateRow(contactId, projectId, db) {
|
|
1089
|
+
const persisted = db.query(`SELECT contact_id, project_id, linked, revision
|
|
1090
|
+
FROM contact_project_membership_states
|
|
1091
|
+
WHERE contact_id = ? AND project_id = ?`).get(contactId, projectId);
|
|
1092
|
+
if (persisted)
|
|
1093
|
+
return persisted;
|
|
1094
|
+
const linked = Boolean(db.query(`SELECT 1 AS present FROM contact_projects WHERE contact_id = ? AND project_id = ?`).get(contactId, projectId));
|
|
1095
|
+
return { contact_id: contactId, project_id: projectId, linked: linked ? 1 : 0, revision: 0 };
|
|
1096
|
+
}
|
|
1097
|
+
function snapshot(row) {
|
|
1098
|
+
const linked = Boolean(row.linked);
|
|
1099
|
+
return {
|
|
1100
|
+
contact_id: row.contact_id,
|
|
1101
|
+
project_id: row.project_id,
|
|
1102
|
+
linked,
|
|
1103
|
+
version: membershipVersion(row.contact_id, row.project_id, linked, row.revision)
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function required(value, name) {
|
|
1107
|
+
const normalized = value.trim();
|
|
1108
|
+
if (!normalized)
|
|
1109
|
+
throw new Error(`${name} is required`);
|
|
1110
|
+
return normalized;
|
|
1111
|
+
}
|
|
1112
|
+
function normalizeInput(input) {
|
|
1113
|
+
return {
|
|
1114
|
+
contact_id: required(input.contact_id, "contact_id"),
|
|
1115
|
+
project_id: required(input.project_id, "project_id"),
|
|
1116
|
+
operation_id: required(input.operation_id, "operation_id"),
|
|
1117
|
+
step_id: required(input.step_id, "step_id"),
|
|
1118
|
+
expected_version: required(input.expected_version, "expected_version")
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function parseSnapshot(value) {
|
|
1122
|
+
return JSON.parse(value);
|
|
1123
|
+
}
|
|
1124
|
+
function replay(row, direction, input) {
|
|
1125
|
+
if (row.direction !== direction || row.contact_id !== input.contact_id || row.project_id !== input.project_id || row.expected_version !== input.expected_version) {
|
|
1126
|
+
throw new ContactProjectMembershipConflictError(`operation_id/step_id already accepted for a different contact-project membership mutation`);
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
outcome: "duplicate_of_accepted",
|
|
1130
|
+
operation_id: row.operation_id,
|
|
1131
|
+
step_id: row.step_id,
|
|
1132
|
+
before: parseSnapshot(row.before_json),
|
|
1133
|
+
after: parseSnapshot(row.after_json),
|
|
1134
|
+
receipt_id: row.receipt_id
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function transitionWithoutReceipt(contactId, projectId, linked, db) {
|
|
1138
|
+
const beforeRow = stateRow(contactId, projectId, db);
|
|
1139
|
+
const changed = Boolean(beforeRow.linked) !== linked;
|
|
1140
|
+
const afterRow = {
|
|
1141
|
+
...beforeRow,
|
|
1142
|
+
linked: linked ? 1 : 0,
|
|
1143
|
+
revision: changed ? beforeRow.revision + 1 : beforeRow.revision
|
|
1144
|
+
};
|
|
1145
|
+
const changedAt = now();
|
|
1146
|
+
db.run(`INSERT INTO contact_project_membership_states
|
|
1147
|
+
(contact_id, project_id, linked, revision, updated_at)
|
|
1148
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1149
|
+
ON CONFLICT(contact_id, project_id) DO UPDATE SET
|
|
1150
|
+
linked = excluded.linked,
|
|
1151
|
+
revision = excluded.revision,
|
|
1152
|
+
updated_at = excluded.updated_at`, [contactId, projectId, afterRow.linked, afterRow.revision, changedAt]);
|
|
1153
|
+
if (linked) {
|
|
1154
|
+
db.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, projectId]);
|
|
1155
|
+
} else {
|
|
1156
|
+
db.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [contactId, projectId]);
|
|
1157
|
+
}
|
|
1158
|
+
return changed;
|
|
1159
|
+
}
|
|
1160
|
+
function setContactProjectMembershipWithoutReceipt(contactId, projectId, linked, db = getDatabase()) {
|
|
1161
|
+
const normalizedContactId = required(contactId, "contact_id");
|
|
1162
|
+
const normalizedProjectId = required(projectId, "project_id");
|
|
1163
|
+
return db.transaction(() => transitionWithoutReceipt(normalizedContactId, normalizedProjectId, linked, db));
|
|
1164
|
+
}
|
|
1165
|
+
function replaceContactProjectMembershipsWithoutReceipts(contactId, projectIds, db = getDatabase()) {
|
|
1166
|
+
const normalizedContactId = required(contactId, "contact_id");
|
|
1167
|
+
const normalizedProjectIds = [...new Set(projectIds.map((projectId) => required(projectId, "project_id")))];
|
|
1168
|
+
return db.transaction(() => {
|
|
1169
|
+
const currentRows = db.query(`SELECT project_id FROM contact_projects WHERE contact_id = ?`).all(normalizedContactId);
|
|
1170
|
+
const desired = new Set(normalizedProjectIds);
|
|
1171
|
+
const population = new Set([...currentRows.map((row) => row.project_id), ...normalizedProjectIds]);
|
|
1172
|
+
for (const projectId of population) {
|
|
1173
|
+
transitionWithoutReceipt(normalizedContactId, projectId, desired.has(projectId), db);
|
|
1174
|
+
}
|
|
1175
|
+
return normalizedProjectIds;
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
function readContactProjectMembership(contactId, projectId, db = getDatabase()) {
|
|
1179
|
+
return snapshot(stateRow(required(contactId, "contact_id"), required(projectId, "project_id"), db));
|
|
1180
|
+
}
|
|
1181
|
+
function listContactProjectMemberships(projectId, maxItems, db = getDatabase()) {
|
|
1182
|
+
const normalizedProjectId = required(projectId, "project_id");
|
|
1183
|
+
if (!Number.isInteger(maxItems) || maxItems < 1)
|
|
1184
|
+
throw new Error("max_items must be a positive integer");
|
|
1185
|
+
const rows = db.query(`SELECT cp.contact_id, cp.project_id,
|
|
1186
|
+
COALESCE(state.linked, 1) AS linked,
|
|
1187
|
+
COALESCE(state.revision, 0) AS revision
|
|
1188
|
+
FROM contact_projects cp
|
|
1189
|
+
LEFT JOIN contact_project_membership_states state
|
|
1190
|
+
ON state.contact_id = cp.contact_id AND state.project_id = cp.project_id
|
|
1191
|
+
WHERE cp.project_id = ?
|
|
1192
|
+
ORDER BY cp.contact_id ASC
|
|
1193
|
+
LIMIT ?`).all(normalizedProjectId, maxItems + 1);
|
|
1194
|
+
if (rows.length > maxItems) {
|
|
1195
|
+
throw new Error(`contact project membership collection exceeds max_items=${maxItems}`);
|
|
1196
|
+
}
|
|
1197
|
+
const contactIds = rows.map((row) => row.contact_id);
|
|
1198
|
+
return {
|
|
1199
|
+
project_id: normalizedProjectId,
|
|
1200
|
+
contact_ids: contactIds,
|
|
1201
|
+
complete: true,
|
|
1202
|
+
membership_revision: `cpml_${digest(JSON.stringify(rows.map((row) => snapshot(row)))).slice(0, 32)}`
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function mutateContactProjectMembership(direction, rawInput, db = getDatabase()) {
|
|
1206
|
+
const input = normalizeInput(rawInput);
|
|
1207
|
+
return db.transaction(() => {
|
|
1208
|
+
const existingReceipt = db.query(`SELECT direction, contact_id, project_id, operation_id, step_id, expected_version,
|
|
1209
|
+
before_json, after_json, receipt_id
|
|
1210
|
+
FROM contact_project_membership_receipts
|
|
1211
|
+
WHERE operation_id = ? AND step_id = ?`).get(input.operation_id, input.step_id);
|
|
1212
|
+
if (existingReceipt)
|
|
1213
|
+
return replay(existingReceipt, direction, input);
|
|
1214
|
+
const contact = db.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
|
|
1215
|
+
if (!contact)
|
|
1216
|
+
throw new Error(`contact not found: ${input.contact_id}`);
|
|
1217
|
+
const beforeRow = stateRow(input.contact_id, input.project_id, db);
|
|
1218
|
+
const before = snapshot(beforeRow);
|
|
1219
|
+
if (before.version !== input.expected_version) {
|
|
1220
|
+
throw new ContactProjectMembershipConflictError(`contact project membership expected_version conflict: expected ${input.expected_version}, current ${before.version}`);
|
|
1221
|
+
}
|
|
1222
|
+
const desiredLinked = direction === "attach";
|
|
1223
|
+
const changed = before.linked !== desiredLinked;
|
|
1224
|
+
const afterRow = {
|
|
1225
|
+
...beforeRow,
|
|
1226
|
+
linked: desiredLinked ? 1 : 0,
|
|
1227
|
+
revision: changed ? beforeRow.revision + 1 : beforeRow.revision
|
|
1228
|
+
};
|
|
1229
|
+
const changedAt = now();
|
|
1230
|
+
db.run(`INSERT INTO contact_project_membership_states
|
|
1231
|
+
(contact_id, project_id, linked, revision, updated_at)
|
|
1232
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1233
|
+
ON CONFLICT(contact_id, project_id) DO UPDATE SET
|
|
1234
|
+
linked = excluded.linked,
|
|
1235
|
+
revision = excluded.revision,
|
|
1236
|
+
updated_at = excluded.updated_at`, [input.contact_id, input.project_id, afterRow.linked, afterRow.revision, changedAt]);
|
|
1237
|
+
if (desiredLinked) {
|
|
1238
|
+
db.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [input.contact_id, input.project_id]);
|
|
1239
|
+
} else {
|
|
1240
|
+
db.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [input.contact_id, input.project_id]);
|
|
1241
|
+
}
|
|
1242
|
+
const after = snapshot(afterRow);
|
|
1243
|
+
const id = receiptId(input);
|
|
1244
|
+
db.run(`INSERT INTO contact_project_membership_receipts (
|
|
1245
|
+
receipt_id, direction, contact_id, project_id, operation_id, step_id,
|
|
1246
|
+
expected_version, before_json, after_json, created_at
|
|
1247
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1248
|
+
id,
|
|
1249
|
+
direction,
|
|
1250
|
+
input.contact_id,
|
|
1251
|
+
input.project_id,
|
|
1252
|
+
input.operation_id,
|
|
1253
|
+
input.step_id,
|
|
1254
|
+
input.expected_version,
|
|
1255
|
+
JSON.stringify(before),
|
|
1256
|
+
JSON.stringify(after),
|
|
1257
|
+
changedAt
|
|
1258
|
+
]);
|
|
1259
|
+
return {
|
|
1260
|
+
outcome: changed ? "accepted" : "duplicate_of_accepted",
|
|
1261
|
+
operation_id: input.operation_id,
|
|
1262
|
+
step_id: input.step_id,
|
|
1263
|
+
before,
|
|
1264
|
+
after,
|
|
1265
|
+
receipt_id: id
|
|
1266
|
+
};
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
var init_project_memberships2 = __esm(() => {
|
|
1270
|
+
init_database();
|
|
1271
|
+
init_project_memberships();
|
|
1272
|
+
});
|
|
1273
|
+
|
|
856
1274
|
// src/db/contacts.ts
|
|
857
1275
|
var exports_contacts = {};
|
|
858
1276
|
__export(exports_contacts, {
|
|
@@ -1451,34 +1869,32 @@ function autoLinkContactToCompany(contactId, db) {
|
|
|
1451
1869
|
}
|
|
1452
1870
|
function linkContactToProject(contactId, projectId, db) {
|
|
1453
1871
|
const d = db || getDatabase();
|
|
1454
|
-
|
|
1872
|
+
setContactProjectMembershipWithoutReceipt(contactId, projectId, true, d);
|
|
1455
1873
|
}
|
|
1456
1874
|
function unlinkContactFromProject(contactId, projectId, db) {
|
|
1457
1875
|
const d = db || getDatabase();
|
|
1458
|
-
|
|
1876
|
+
setContactProjectMembershipWithoutReceipt(contactId, projectId, false, d);
|
|
1459
1877
|
}
|
|
1460
1878
|
function getContactProjectIds(contactId, db) {
|
|
1461
1879
|
const d = db || getDatabase();
|
|
1462
|
-
const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id =
|
|
1880
|
+
const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id = ? ORDER BY project_id ASC`).all(contactId);
|
|
1463
1881
|
return rows.map((r) => r.project_id);
|
|
1464
1882
|
}
|
|
1465
1883
|
function listContactIdsByProject(projectId, db) {
|
|
1466
1884
|
const d = db || getDatabase();
|
|
1467
|
-
const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id =
|
|
1885
|
+
const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id = ? ORDER BY contact_id ASC`).all(projectId);
|
|
1468
1886
|
return rows.map((r) => r.contact_id);
|
|
1469
1887
|
}
|
|
1470
1888
|
function setContactProjects(contactId, projectIds, db) {
|
|
1471
1889
|
const d = db || getDatabase();
|
|
1472
|
-
|
|
1473
|
-
for (const pid of projectIds) {
|
|
1474
|
-
d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, pid]);
|
|
1475
|
-
}
|
|
1890
|
+
replaceContactProjectMembershipsWithoutReceipts(contactId, projectIds, d);
|
|
1476
1891
|
}
|
|
1477
1892
|
var init_contacts = __esm(() => {
|
|
1478
1893
|
init_types();
|
|
1479
1894
|
init_database();
|
|
1480
1895
|
init_activity();
|
|
1481
1896
|
init_tombstones();
|
|
1897
|
+
init_project_memberships2();
|
|
1482
1898
|
});
|
|
1483
1899
|
|
|
1484
1900
|
// src/db/events.ts
|
|
@@ -1644,6 +2060,7 @@ function getStorageStatus(db = getDatabase()) {
|
|
|
1644
2060
|
|
|
1645
2061
|
// src/store/index.ts
|
|
1646
2062
|
init_contacts();
|
|
2063
|
+
init_project_memberships2();
|
|
1647
2064
|
|
|
1648
2065
|
// src/db/companies.ts
|
|
1649
2066
|
init_types();
|
|
@@ -3172,13 +3589,24 @@ init_database();
|
|
|
3172
3589
|
// src/lib/vault.ts
|
|
3173
3590
|
init_database();
|
|
3174
3591
|
import { existsSync as existsSync3, readFileSync, writeFileSync, mkdirSync as mkdirSync3, unlinkSync } from "fs";
|
|
3175
|
-
import { join as
|
|
3176
|
-
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
|
|
3177
|
-
var VAULT_DIR = getDataDir();
|
|
3178
|
-
var VAULT_CONFIG = join2(VAULT_DIR, "vault.json");
|
|
3179
|
-
var VAULT_SESSION = join2(VAULT_DIR, ".vault-session");
|
|
3180
|
-
var DOCUMENTS_DIR = join2(VAULT_DIR, "documents");
|
|
3592
|
+
import { join as join3 } from "path";
|
|
3593
|
+
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash as createHash2 } from "crypto";
|
|
3181
3594
|
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
3595
|
+
function getVaultDir() {
|
|
3596
|
+
return getDataDir();
|
|
3597
|
+
}
|
|
3598
|
+
function getVaultConfigPath() {
|
|
3599
|
+
return join3(getDataDir(), "vault.json");
|
|
3600
|
+
}
|
|
3601
|
+
function getVaultSessionPath() {
|
|
3602
|
+
return join3(getStateDir(), ".vault-session");
|
|
3603
|
+
}
|
|
3604
|
+
function getDocumentsDir() {
|
|
3605
|
+
const dir = join3(getDataDir(), "documents");
|
|
3606
|
+
if (!existsSync3(dir))
|
|
3607
|
+
mkdirSync3(dir, { recursive: true });
|
|
3608
|
+
return dir;
|
|
3609
|
+
}
|
|
3182
3610
|
var _derivedKey = null;
|
|
3183
3611
|
function deriveKey(passphrase, salt) {
|
|
3184
3612
|
return pbkdf2Sync(passphrase, salt, 1e5, 32, "sha512");
|
|
@@ -3188,16 +3616,17 @@ function saveSession(key) {
|
|
|
3188
3616
|
key: key.toString("hex"),
|
|
3189
3617
|
expires_at: new Date(Date.now() + SESSION_TTL_MS).toISOString()
|
|
3190
3618
|
};
|
|
3191
|
-
writeFileSync(
|
|
3619
|
+
writeFileSync(getVaultSessionPath(), JSON.stringify(session), { mode: 384 });
|
|
3192
3620
|
}
|
|
3193
3621
|
function loadSession() {
|
|
3194
|
-
|
|
3622
|
+
const sessionPath = getVaultSessionPath();
|
|
3623
|
+
if (!existsSync3(sessionPath))
|
|
3195
3624
|
return null;
|
|
3196
3625
|
try {
|
|
3197
|
-
const session = JSON.parse(readFileSync(
|
|
3626
|
+
const session = JSON.parse(readFileSync(sessionPath, "utf-8"));
|
|
3198
3627
|
if (new Date(session.expires_at).getTime() < Date.now()) {
|
|
3199
3628
|
try {
|
|
3200
|
-
unlinkSync(
|
|
3629
|
+
unlinkSync(sessionPath);
|
|
3201
3630
|
} catch {}
|
|
3202
3631
|
return null;
|
|
3203
3632
|
}
|
|
@@ -3208,33 +3637,33 @@ function loadSession() {
|
|
|
3208
3637
|
}
|
|
3209
3638
|
function clearSession() {
|
|
3210
3639
|
try {
|
|
3211
|
-
if (existsSync3(
|
|
3212
|
-
unlinkSync(
|
|
3640
|
+
if (existsSync3(getVaultSessionPath()))
|
|
3641
|
+
unlinkSync(getVaultSessionPath());
|
|
3213
3642
|
} catch {}
|
|
3214
3643
|
}
|
|
3215
3644
|
function initVault(passphrase) {
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3645
|
+
const vaultDir = getVaultDir();
|
|
3646
|
+
if (!existsSync3(vaultDir))
|
|
3647
|
+
mkdirSync3(vaultDir, { recursive: true });
|
|
3648
|
+
mkdirSync3(getDocumentsDir(), { recursive: true });
|
|
3220
3649
|
const salt = randomBytes(32);
|
|
3221
3650
|
const key = deriveKey(passphrase, salt);
|
|
3222
|
-
const keyHash =
|
|
3651
|
+
const keyHash = createHash2("sha256").update(key).digest("hex");
|
|
3223
3652
|
const config = { salt: salt.toString("hex"), key_hash: keyHash, created_at: new Date().toISOString() };
|
|
3224
|
-
writeFileSync(
|
|
3653
|
+
writeFileSync(getVaultConfigPath(), JSON.stringify(config, null, 2));
|
|
3225
3654
|
_derivedKey = key;
|
|
3226
3655
|
saveSession(key);
|
|
3227
3656
|
}
|
|
3228
3657
|
function isVaultInitialized() {
|
|
3229
|
-
return existsSync3(
|
|
3658
|
+
return existsSync3(getVaultConfigPath());
|
|
3230
3659
|
}
|
|
3231
3660
|
function unlockVault(passphrase) {
|
|
3232
|
-
if (!existsSync3(
|
|
3661
|
+
if (!existsSync3(getVaultConfigPath()))
|
|
3233
3662
|
throw new Error("Vault not initialized. Run 'contacts vault init' first.");
|
|
3234
|
-
const config = JSON.parse(readFileSync(
|
|
3663
|
+
const config = JSON.parse(readFileSync(getVaultConfigPath(), "utf-8"));
|
|
3235
3664
|
const salt = Buffer.from(config.salt, "hex");
|
|
3236
3665
|
const key = deriveKey(passphrase, salt);
|
|
3237
|
-
const keyHash =
|
|
3666
|
+
const keyHash = createHash2("sha256").update(key).digest("hex");
|
|
3238
3667
|
if (keyHash !== config.key_hash)
|
|
3239
3668
|
return false;
|
|
3240
3669
|
_derivedKey = key;
|
|
@@ -3286,10 +3715,11 @@ function decrypt(ciphertext, iv) {
|
|
|
3286
3715
|
return decrypted;
|
|
3287
3716
|
}
|
|
3288
3717
|
function storeFile(sourcePath, entityId) {
|
|
3289
|
-
|
|
3290
|
-
|
|
3718
|
+
const documentsDir = getDocumentsDir();
|
|
3719
|
+
if (!existsSync3(documentsDir))
|
|
3720
|
+
mkdirSync3(documentsDir, { recursive: true });
|
|
3291
3721
|
const ext = sourcePath.split(".").pop() || "bin";
|
|
3292
|
-
const destPath =
|
|
3722
|
+
const destPath = join3(documentsDir, `${entityId}.${ext}`);
|
|
3293
3723
|
const data = readFileSync(sourcePath);
|
|
3294
3724
|
writeFileSync(destPath, data);
|
|
3295
3725
|
return destPath;
|
|
@@ -4183,8 +4613,8 @@ async function assembleContext(contactIds, format = "meeting_prep", db) {
|
|
|
4183
4613
|
// src/lib/images.ts
|
|
4184
4614
|
init_database();
|
|
4185
4615
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, unlinkSync as unlinkSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4186
|
-
import { join as
|
|
4187
|
-
var IMAGES_DIR =
|
|
4616
|
+
import { join as join4, extname, basename } from "path";
|
|
4617
|
+
var IMAGES_DIR = join4(getDataDir(), "images");
|
|
4188
4618
|
function ensureImagesDir() {
|
|
4189
4619
|
if (!existsSync5(IMAGES_DIR))
|
|
4190
4620
|
mkdirSync4(IMAGES_DIR, { recursive: true });
|
|
@@ -4197,14 +4627,14 @@ function saveImage(entityId, source, options) {
|
|
|
4197
4627
|
const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
|
|
4198
4628
|
const data = Buffer.from(base64Match[2], "base64");
|
|
4199
4629
|
const filename2 = `${entityId}.${ext2}`;
|
|
4200
|
-
writeFileSync2(
|
|
4630
|
+
writeFileSync2(join4(IMAGES_DIR, filename2), data);
|
|
4201
4631
|
return filename2;
|
|
4202
4632
|
}
|
|
4203
4633
|
if (!existsSync5(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
|
|
4204
4634
|
const ext2 = options?.format || "jpg";
|
|
4205
4635
|
const data = Buffer.from(source.trim(), "base64");
|
|
4206
4636
|
const filename2 = `${entityId}.${ext2}`;
|
|
4207
|
-
writeFileSync2(
|
|
4637
|
+
writeFileSync2(join4(IMAGES_DIR, filename2), data);
|
|
4208
4638
|
return filename2;
|
|
4209
4639
|
}
|
|
4210
4640
|
if (!existsSync5(source)) {
|
|
@@ -4216,14 +4646,14 @@ function saveImage(entityId, source, options) {
|
|
|
4216
4646
|
throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
|
|
4217
4647
|
}
|
|
4218
4648
|
const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
4219
|
-
copyFileSync2(source,
|
|
4649
|
+
copyFileSync2(source, join4(IMAGES_DIR, filename));
|
|
4220
4650
|
return filename;
|
|
4221
4651
|
}
|
|
4222
4652
|
function getImagePath(entityId) {
|
|
4223
4653
|
ensureImagesDir();
|
|
4224
4654
|
const files = readdirSync2(IMAGES_DIR);
|
|
4225
4655
|
const match = files.find((f) => f.startsWith(`${entityId}.`));
|
|
4226
|
-
return match ?
|
|
4656
|
+
return match ? join4(IMAGES_DIR, match) : null;
|
|
4227
4657
|
}
|
|
4228
4658
|
function getImageAsBase64(entityId) {
|
|
4229
4659
|
const path = getImagePath(entityId);
|
|
@@ -4240,7 +4670,7 @@ function deleteImage(entityId) {
|
|
|
4240
4670
|
let deleted = false;
|
|
4241
4671
|
for (const f of files) {
|
|
4242
4672
|
if (f.startsWith(`${entityId}.`)) {
|
|
4243
|
-
unlinkSync3(
|
|
4673
|
+
unlinkSync3(join4(IMAGES_DIR, f));
|
|
4244
4674
|
deleted = true;
|
|
4245
4675
|
}
|
|
4246
4676
|
}
|
|
@@ -4252,7 +4682,7 @@ function listImages() {
|
|
|
4252
4682
|
return files.map((f) => ({
|
|
4253
4683
|
entity_id: basename(f, extname(f)),
|
|
4254
4684
|
filename: f,
|
|
4255
|
-
path:
|
|
4685
|
+
path: join4(IMAGES_DIR, f)
|
|
4256
4686
|
}));
|
|
4257
4687
|
}
|
|
4258
4688
|
|
|
@@ -4749,6 +5179,15 @@ class LocalStore {
|
|
|
4749
5179
|
async listContactIdsByProject(projectId) {
|
|
4750
5180
|
return listContactIdsByProject(projectId, this.db);
|
|
4751
5181
|
}
|
|
5182
|
+
async readContactProjectMembership(contactId, projectId) {
|
|
5183
|
+
return readContactProjectMembership(contactId, projectId, this.db);
|
|
5184
|
+
}
|
|
5185
|
+
async listContactProjectMemberships(projectId, maxItems) {
|
|
5186
|
+
return listContactProjectMemberships(projectId, maxItems, this.db);
|
|
5187
|
+
}
|
|
5188
|
+
async mutateContactProjectMembership(direction, input) {
|
|
5189
|
+
return mutateContactProjectMembership(direction, input, this.db);
|
|
5190
|
+
}
|
|
4752
5191
|
async createCompany(input) {
|
|
4753
5192
|
return createCompany(input, this.db);
|
|
4754
5193
|
}
|
|
@@ -5371,20 +5810,33 @@ class ApiStore {
|
|
|
5371
5810
|
async findContactByEmailAddress(address) {
|
|
5372
5811
|
return this.getContactByEmail(address);
|
|
5373
5812
|
}
|
|
5374
|
-
async linkContactToProject() {
|
|
5375
|
-
|
|
5813
|
+
async linkContactToProject(contactId, projectId) {
|
|
5814
|
+
await this.client.transport.put(`/contacts/${this.enc(contactId)}/projects/${this.enc(projectId)}`);
|
|
5815
|
+
}
|
|
5816
|
+
async unlinkContactFromProject(contactId, projectId) {
|
|
5817
|
+
await this.del(`/contacts/${this.enc(contactId)}/projects/${this.enc(projectId)}`);
|
|
5818
|
+
}
|
|
5819
|
+
async getContactProjectIds(contactId) {
|
|
5820
|
+
return pick(await this.g(`/contacts/${this.enc(contactId)}/projects`), "project_ids") ?? [];
|
|
5821
|
+
}
|
|
5822
|
+
async setContactProjects(contactId, projectIds) {
|
|
5823
|
+
await this.client.transport.put(`/contacts/${this.enc(contactId)}/projects`, { project_ids: projectIds });
|
|
5376
5824
|
}
|
|
5377
|
-
async
|
|
5378
|
-
return
|
|
5825
|
+
async listContactIdsByProject(projectId) {
|
|
5826
|
+
return pick(await this.g(`/projects/${this.enc(projectId)}/contacts`), "contact_ids") ?? [];
|
|
5379
5827
|
}
|
|
5380
|
-
async
|
|
5381
|
-
return
|
|
5828
|
+
async readContactProjectMembership(contactId, projectId) {
|
|
5829
|
+
return this.client.transport.get(`/projects/${this.enc(projectId)}/contact-memberships/${this.enc(contactId)}`);
|
|
5382
5830
|
}
|
|
5383
|
-
async
|
|
5384
|
-
return
|
|
5831
|
+
async listContactProjectMemberships(projectId, maxItems) {
|
|
5832
|
+
return this.client.transport.get(`/projects/${this.enc(projectId)}/contact-memberships`, { query: { max_items: maxItems } });
|
|
5385
5833
|
}
|
|
5386
|
-
async
|
|
5387
|
-
return
|
|
5834
|
+
async mutateContactProjectMembership(direction, input) {
|
|
5835
|
+
return this.client.transport.post(`/projects/${this.enc(input.project_id)}/contact-memberships/${this.enc(input.contact_id)}/${direction}`, {
|
|
5836
|
+
operation_id: input.operation_id,
|
|
5837
|
+
step_id: input.step_id,
|
|
5838
|
+
expected_version: input.expected_version
|
|
5839
|
+
});
|
|
5388
5840
|
}
|
|
5389
5841
|
async createCompany(input) {
|
|
5390
5842
|
const res = await this.client.create("companies", stripUndefined(input));
|
|
@@ -5907,6 +6359,7 @@ function resetStoreCache() {
|
|
|
5907
6359
|
}
|
|
5908
6360
|
|
|
5909
6361
|
// src/index.ts
|
|
6362
|
+
init_project_memberships();
|
|
5910
6363
|
init_types();
|
|
5911
6364
|
|
|
5912
6365
|
// src/sdk/v1.generated.ts
|
|
@@ -6013,6 +6466,34 @@ class ContactsV1Client {
|
|
|
6013
6466
|
init
|
|
6014
6467
|
});
|
|
6015
6468
|
}
|
|
6469
|
+
async getContactProjectIds(contactId, init) {
|
|
6470
|
+
return this.request("GET", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects`, {
|
|
6471
|
+
body: undefined,
|
|
6472
|
+
query: undefined,
|
|
6473
|
+
init
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
async setContactProjects(contactId, body, init) {
|
|
6477
|
+
return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects`, {
|
|
6478
|
+
body,
|
|
6479
|
+
query: undefined,
|
|
6480
|
+
init
|
|
6481
|
+
});
|
|
6482
|
+
}
|
|
6483
|
+
async linkContactToProject(contactId, projectId, init) {
|
|
6484
|
+
return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects/${encodeURIComponent(String(projectId))}`, {
|
|
6485
|
+
body: undefined,
|
|
6486
|
+
query: undefined,
|
|
6487
|
+
init
|
|
6488
|
+
});
|
|
6489
|
+
}
|
|
6490
|
+
async unlinkContactFromProject(contactId, projectId, init) {
|
|
6491
|
+
return this.request("DELETE", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects/${encodeURIComponent(String(projectId))}`, {
|
|
6492
|
+
body: undefined,
|
|
6493
|
+
query: undefined,
|
|
6494
|
+
init
|
|
6495
|
+
});
|
|
6496
|
+
}
|
|
6016
6497
|
async addTagToContact(contactId, tagId, init) {
|
|
6017
6498
|
return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/tags/${encodeURIComponent(String(tagId))}`, {
|
|
6018
6499
|
body: undefined,
|
|
@@ -6048,6 +6529,41 @@ class ContactsV1Client {
|
|
|
6048
6529
|
init
|
|
6049
6530
|
});
|
|
6050
6531
|
}
|
|
6532
|
+
async listContactProjectMemberships(projectId, query, init) {
|
|
6533
|
+
return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships`, {
|
|
6534
|
+
body: undefined,
|
|
6535
|
+
query,
|
|
6536
|
+
init
|
|
6537
|
+
});
|
|
6538
|
+
}
|
|
6539
|
+
async readContactProjectMembership(projectId, contactId, init) {
|
|
6540
|
+
return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}`, {
|
|
6541
|
+
body: undefined,
|
|
6542
|
+
query: undefined,
|
|
6543
|
+
init
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6546
|
+
async attachContactProjectMembership(projectId, contactId, body, init) {
|
|
6547
|
+
return this.request("POST", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}/attach`, {
|
|
6548
|
+
body,
|
|
6549
|
+
query: undefined,
|
|
6550
|
+
init
|
|
6551
|
+
});
|
|
6552
|
+
}
|
|
6553
|
+
async detachContactProjectMembership(projectId, contactId, body, init) {
|
|
6554
|
+
return this.request("POST", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}/detach`, {
|
|
6555
|
+
body,
|
|
6556
|
+
query: undefined,
|
|
6557
|
+
init
|
|
6558
|
+
});
|
|
6559
|
+
}
|
|
6560
|
+
async listContactIdsByProject(projectId, init) {
|
|
6561
|
+
return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contacts`, {
|
|
6562
|
+
body: undefined,
|
|
6563
|
+
query: undefined,
|
|
6564
|
+
init
|
|
6565
|
+
});
|
|
6566
|
+
}
|
|
6051
6567
|
async getStats(init) {
|
|
6052
6568
|
return this.request("GET", `/v1/stats`, {
|
|
6053
6569
|
body: undefined,
|
|
@@ -6093,7 +6609,7 @@ class ContactsV1Client {
|
|
|
6093
6609
|
}
|
|
6094
6610
|
// src/lib/package-version.ts
|
|
6095
6611
|
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
6096
|
-
import { dirname as dirname2, join as
|
|
6612
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
6097
6613
|
import { fileURLToPath } from "url";
|
|
6098
6614
|
var cached2 = null;
|
|
6099
6615
|
function getPackageVersion() {
|
|
@@ -6102,7 +6618,7 @@ function getPackageVersion() {
|
|
|
6102
6618
|
try {
|
|
6103
6619
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
6104
6620
|
for (let i = 0;i < 8; i++) {
|
|
6105
|
-
const pkgPath =
|
|
6621
|
+
const pkgPath = join5(dir, "package.json");
|
|
6106
6622
|
if (existsSync6(pkgPath)) {
|
|
6107
6623
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
6108
6624
|
if (pkg.name === "@hasna/contacts" && pkg.version) {
|
|
@@ -6258,6 +6774,58 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
6258
6774
|
color: { type: "string" },
|
|
6259
6775
|
description: { type: "string" }
|
|
6260
6776
|
}
|
|
6777
|
+
},
|
|
6778
|
+
ProjectIdsInput: {
|
|
6779
|
+
type: "object",
|
|
6780
|
+
required: ["project_ids"],
|
|
6781
|
+
properties: {
|
|
6782
|
+
project_ids: {
|
|
6783
|
+
type: "array",
|
|
6784
|
+
items: { type: "string", minLength: 1 },
|
|
6785
|
+
uniqueItems: true
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
},
|
|
6789
|
+
ContactProjectMembershipSnapshot: {
|
|
6790
|
+
type: "object",
|
|
6791
|
+
required: ["contact_id", "project_id", "linked", "version"],
|
|
6792
|
+
properties: {
|
|
6793
|
+
contact_id: { type: "string" },
|
|
6794
|
+
project_id: { type: "string" },
|
|
6795
|
+
linked: { type: "boolean" },
|
|
6796
|
+
version: { type: "string" }
|
|
6797
|
+
}
|
|
6798
|
+
},
|
|
6799
|
+
ContactProjectMembershipMutationInput: {
|
|
6800
|
+
type: "object",
|
|
6801
|
+
required: ["operation_id", "step_id", "expected_version"],
|
|
6802
|
+
properties: {
|
|
6803
|
+
operation_id: { type: "string", minLength: 1 },
|
|
6804
|
+
step_id: { type: "string", minLength: 1 },
|
|
6805
|
+
expected_version: { type: "string", minLength: 1 }
|
|
6806
|
+
}
|
|
6807
|
+
},
|
|
6808
|
+
ContactProjectMembershipMutationResult: {
|
|
6809
|
+
type: "object",
|
|
6810
|
+
required: ["outcome", "operation_id", "step_id", "before", "after", "receipt_id"],
|
|
6811
|
+
properties: {
|
|
6812
|
+
outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
|
|
6813
|
+
operation_id: { type: "string" },
|
|
6814
|
+
step_id: { type: "string" },
|
|
6815
|
+
before: { $ref: "#/components/schemas/ContactProjectMembershipSnapshot" },
|
|
6816
|
+
after: { $ref: "#/components/schemas/ContactProjectMembershipSnapshot" },
|
|
6817
|
+
receipt_id: { type: "string" }
|
|
6818
|
+
}
|
|
6819
|
+
},
|
|
6820
|
+
ContactProjectMembershipListResult: {
|
|
6821
|
+
type: "object",
|
|
6822
|
+
required: ["project_id", "contact_ids", "complete", "membership_revision"],
|
|
6823
|
+
properties: {
|
|
6824
|
+
project_id: { type: "string" },
|
|
6825
|
+
contact_ids: { type: "array", items: { type: "string" } },
|
|
6826
|
+
complete: { type: "boolean", const: true },
|
|
6827
|
+
membership_revision: { type: "string" }
|
|
6828
|
+
}
|
|
6261
6829
|
}
|
|
6262
6830
|
}
|
|
6263
6831
|
},
|
|
@@ -6458,6 +7026,167 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
6458
7026
|
})
|
|
6459
7027
|
}
|
|
6460
7028
|
},
|
|
7029
|
+
"/v1/contacts/{contact_id}/projects": {
|
|
7030
|
+
get: {
|
|
7031
|
+
operationId: "getContactProjectIds",
|
|
7032
|
+
summary: "List project ids attached to a contact",
|
|
7033
|
+
parameters: [
|
|
7034
|
+
{ name: "contact_id", in: "path", required: true, schema: { type: "string" } }
|
|
7035
|
+
],
|
|
7036
|
+
responses: objResponse({
|
|
7037
|
+
contact_id: { type: "string" },
|
|
7038
|
+
project_ids: { type: "array", items: { type: "string" } }
|
|
7039
|
+
})
|
|
7040
|
+
},
|
|
7041
|
+
put: {
|
|
7042
|
+
operationId: "setContactProjects",
|
|
7043
|
+
summary: "Atomically replace a contact's project memberships",
|
|
7044
|
+
parameters: [
|
|
7045
|
+
{ name: "contact_id", in: "path", required: true, schema: { type: "string" } }
|
|
7046
|
+
],
|
|
7047
|
+
requestBody: {
|
|
7048
|
+
required: true,
|
|
7049
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectIdsInput" } } }
|
|
7050
|
+
},
|
|
7051
|
+
responses: objResponse({
|
|
7052
|
+
contact_id: { type: "string" },
|
|
7053
|
+
project_ids: { type: "array", items: { type: "string" } }
|
|
7054
|
+
})
|
|
7055
|
+
}
|
|
7056
|
+
},
|
|
7057
|
+
"/v1/contacts/{contact_id}/projects/{project_id}": {
|
|
7058
|
+
put: {
|
|
7059
|
+
operationId: "linkContactToProject",
|
|
7060
|
+
summary: "Attach a contact to a project idempotently",
|
|
7061
|
+
parameters: [
|
|
7062
|
+
{ name: "contact_id", in: "path", required: true, schema: { type: "string" } },
|
|
7063
|
+
{ name: "project_id", in: "path", required: true, schema: { type: "string" } }
|
|
7064
|
+
],
|
|
7065
|
+
responses: objResponse({
|
|
7066
|
+
attached: { type: "boolean" },
|
|
7067
|
+
contact_id: { type: "string" },
|
|
7068
|
+
project_id: { type: "string" }
|
|
7069
|
+
})
|
|
7070
|
+
},
|
|
7071
|
+
delete: {
|
|
7072
|
+
operationId: "unlinkContactFromProject",
|
|
7073
|
+
summary: "Detach a contact from a project",
|
|
7074
|
+
parameters: [
|
|
7075
|
+
{ name: "contact_id", in: "path", required: true, schema: { type: "string" } },
|
|
7076
|
+
{ name: "project_id", in: "path", required: true, schema: { type: "string" } }
|
|
7077
|
+
],
|
|
7078
|
+
responses: objResponse({
|
|
7079
|
+
removed: { type: "boolean" },
|
|
7080
|
+
contact_id: { type: "string" },
|
|
7081
|
+
project_id: { type: "string" }
|
|
7082
|
+
})
|
|
7083
|
+
}
|
|
7084
|
+
},
|
|
7085
|
+
"/v1/projects/{project_id}/contacts": {
|
|
7086
|
+
get: {
|
|
7087
|
+
operationId: "listContactIdsByProject",
|
|
7088
|
+
summary: "List contact ids attached to a project",
|
|
7089
|
+
parameters: [
|
|
7090
|
+
{ name: "project_id", in: "path", required: true, schema: { type: "string" } }
|
|
7091
|
+
],
|
|
7092
|
+
responses: objResponse({
|
|
7093
|
+
project_id: { type: "string" },
|
|
7094
|
+
contact_ids: { type: "array", items: { type: "string" } }
|
|
7095
|
+
})
|
|
7096
|
+
}
|
|
7097
|
+
},
|
|
7098
|
+
"/v1/projects/{project_id}/contact-memberships": {
|
|
7099
|
+
get: {
|
|
7100
|
+
operationId: "listContactProjectMemberships",
|
|
7101
|
+
summary: "List the complete authoritative contact membership collection for a project",
|
|
7102
|
+
parameters: [
|
|
7103
|
+
{ name: "project_id", in: "path", required: true, schema: { type: "string" } },
|
|
7104
|
+
{ name: "max_items", in: "query", required: true, schema: { type: "integer", minimum: 1 } }
|
|
7105
|
+
],
|
|
7106
|
+
responses: {
|
|
7107
|
+
"200": {
|
|
7108
|
+
content: {
|
|
7109
|
+
"application/json": {
|
|
7110
|
+
schema: { $ref: "#/components/schemas/ContactProjectMembershipListResult" }
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
},
|
|
7117
|
+
"/v1/projects/{project_id}/contact-memberships/{contact_id}": {
|
|
7118
|
+
get: {
|
|
7119
|
+
operationId: "readContactProjectMembership",
|
|
7120
|
+
summary: "Read one authoritative contact-project membership snapshot",
|
|
7121
|
+
parameters: [
|
|
7122
|
+
{ name: "project_id", in: "path", required: true, schema: { type: "string" } },
|
|
7123
|
+
{ name: "contact_id", in: "path", required: true, schema: { type: "string" } }
|
|
7124
|
+
],
|
|
7125
|
+
responses: {
|
|
7126
|
+
"200": {
|
|
7127
|
+
content: {
|
|
7128
|
+
"application/json": {
|
|
7129
|
+
schema: { $ref: "#/components/schemas/ContactProjectMembershipSnapshot" }
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
},
|
|
7136
|
+
"/v1/projects/{project_id}/contact-memberships/{contact_id}/attach": {
|
|
7137
|
+
post: {
|
|
7138
|
+
operationId: "attachContactProjectMembership",
|
|
7139
|
+
summary: "Attach a contact to a project under expected-version CAS with a replay-safe receipt",
|
|
7140
|
+
parameters: [
|
|
7141
|
+
{ name: "project_id", in: "path", required: true, schema: { type: "string" } },
|
|
7142
|
+
{ name: "contact_id", in: "path", required: true, schema: { type: "string" } }
|
|
7143
|
+
],
|
|
7144
|
+
requestBody: {
|
|
7145
|
+
required: true,
|
|
7146
|
+
content: {
|
|
7147
|
+
"application/json": {
|
|
7148
|
+
schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationInput" }
|
|
7149
|
+
}
|
|
7150
|
+
}
|
|
7151
|
+
},
|
|
7152
|
+
responses: {
|
|
7153
|
+
"200": {
|
|
7154
|
+
content: {
|
|
7155
|
+
"application/json": {
|
|
7156
|
+
schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationResult" }
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
},
|
|
7163
|
+
"/v1/projects/{project_id}/contact-memberships/{contact_id}/detach": {
|
|
7164
|
+
post: {
|
|
7165
|
+
operationId: "detachContactProjectMembership",
|
|
7166
|
+
summary: "Detach a contact from a project under expected-version CAS with a replay-safe receipt",
|
|
7167
|
+
parameters: [
|
|
7168
|
+
{ name: "project_id", in: "path", required: true, schema: { type: "string" } },
|
|
7169
|
+
{ name: "contact_id", in: "path", required: true, schema: { type: "string" } }
|
|
7170
|
+
],
|
|
7171
|
+
requestBody: {
|
|
7172
|
+
required: true,
|
|
7173
|
+
content: {
|
|
7174
|
+
"application/json": {
|
|
7175
|
+
schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationInput" }
|
|
7176
|
+
}
|
|
7177
|
+
}
|
|
7178
|
+
},
|
|
7179
|
+
responses: {
|
|
7180
|
+
"200": {
|
|
7181
|
+
content: {
|
|
7182
|
+
"application/json": {
|
|
7183
|
+
schema: { $ref: "#/components/schemas/ContactProjectMembershipMutationResult" }
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
},
|
|
6461
7190
|
"/v1/stats": {
|
|
6462
7191
|
get: {
|
|
6463
7192
|
operationId: "getStats",
|
|
@@ -6482,6 +7211,7 @@ export {
|
|
|
6482
7211
|
DuplicateAudienceIdError,
|
|
6483
7212
|
ContactsV1Client,
|
|
6484
7213
|
ApiError as ContactsV1ApiError,
|
|
7214
|
+
ContactProjectMembershipConflictError,
|
|
6485
7215
|
ContactNotFoundError,
|
|
6486
7216
|
CompanyNotFoundError,
|
|
6487
7217
|
CONSENT_STATUSES,
|