@hraness/peopleblade 0.1.1 → 0.2.1
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 +60 -7
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/dist/cli.ts +31 -0
- package/dist/cloud-sync-client.js +1836 -0
- package/dist/migrations/016_source_binding_metadata_incarnations.sql +205 -0
- package/dist/migrations/017_person_note_content_revisions.sql +151 -0
- package/dist/peopleblade.js +2704 -690
- package/package.json +3 -2
|
@@ -0,0 +1,1836 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/local/cloud-sync-client.ts
|
|
3
|
+
import { Effect as Effect3 } from "effect";
|
|
4
|
+
|
|
5
|
+
// src/lib/contracts.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
var enrichmentPolicyVersion = "identity-bound-claims-v15";
|
|
9
|
+
var enrichmentPriorityPolicyVersion = "enrichment-value-v9";
|
|
10
|
+
var exactProfileTitleOnlyEvidenceExcerpt = "Title-only result for the exact stored public profile URL; no page text was returned.";
|
|
11
|
+
var sourceLabelSchema = z.enum([
|
|
12
|
+
"apple",
|
|
13
|
+
"beeper",
|
|
14
|
+
"gmail",
|
|
15
|
+
"google",
|
|
16
|
+
"imessage",
|
|
17
|
+
"instagram",
|
|
18
|
+
"linkedin",
|
|
19
|
+
"substack",
|
|
20
|
+
"telegram",
|
|
21
|
+
"whatsapp",
|
|
22
|
+
"x"
|
|
23
|
+
]);
|
|
24
|
+
var boundedText = (maximum) => z.string().trim().min(1).max(maximum);
|
|
25
|
+
var nullableText = (maximum) => boundedText(maximum).nullable();
|
|
26
|
+
var httpUrlSchema = z.url().max(4096).refine((value) => {
|
|
27
|
+
try {
|
|
28
|
+
const protocol = new URL(value).protocol;
|
|
29
|
+
return protocol === "https:" || protocol === "http:";
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}, "URL must use HTTP or HTTPS");
|
|
34
|
+
function isCanonicalBirthday(value) {
|
|
35
|
+
const match = /^(?:(\d{4})-|--)(\d{2})-(\d{2})$/u.exec(value);
|
|
36
|
+
if (match === null)
|
|
37
|
+
return false;
|
|
38
|
+
const year = match[1] === undefined ? 2000 : Number(match[1]);
|
|
39
|
+
const month = Number(match[2]);
|
|
40
|
+
const day = Number(match[3]);
|
|
41
|
+
if (year < 1 || year > 9999 || month < 1 || month > 12 || day < 1)
|
|
42
|
+
return false;
|
|
43
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
44
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
45
|
+
return day <= days[month - 1];
|
|
46
|
+
}
|
|
47
|
+
var birthdaySchema = z.string().refine(isCanonicalBirthday, "Birthday must be a real canonical YYYY-MM-DD or --MM-DD date").nullable();
|
|
48
|
+
var cloudEmailSchema = z.email().max(1024);
|
|
49
|
+
var identityAnchorsSchema = z.discriminatedUnion("state", [
|
|
50
|
+
z.object({
|
|
51
|
+
state: z.literal("available"),
|
|
52
|
+
emails: z.array(cloudEmailSchema).max(100),
|
|
53
|
+
phones: z.array(boundedText(256)).max(100),
|
|
54
|
+
profileUrls: z.array(httpUrlSchema).max(100)
|
|
55
|
+
}).strict(),
|
|
56
|
+
z.object({ state: z.literal("unavailable") }).strict()
|
|
57
|
+
]);
|
|
58
|
+
var automaticEnrichmentSelectionSchema = z.object({
|
|
59
|
+
version: z.literal(1),
|
|
60
|
+
state: z.enum(["eligible", "blocked"])
|
|
61
|
+
}).strict();
|
|
62
|
+
var unavailableAutomaticEnrichmentSelectionSchema = z.object({
|
|
63
|
+
version: z.literal(0),
|
|
64
|
+
state: z.literal("unavailable")
|
|
65
|
+
}).strict();
|
|
66
|
+
var legacyEnrichmentInputSchema = z.object({
|
|
67
|
+
displayName: boundedText(1024),
|
|
68
|
+
emails: z.array(cloudEmailSchema).max(100),
|
|
69
|
+
phones: z.array(boundedText(256)).max(100),
|
|
70
|
+
organization: nullableText(1024),
|
|
71
|
+
title: nullableText(1024),
|
|
72
|
+
sources: z.array(sourceLabelSchema).max(16),
|
|
73
|
+
providerHandles: z.record(boundedText(128), boundedText(2048)).refine((value) => Object.keys(value).length <= 50, "providerHandles contains too many entries")
|
|
74
|
+
}).strict();
|
|
75
|
+
var enrichmentInputSchema = legacyEnrichmentInputSchema.extend({
|
|
76
|
+
enrichmentInputVersion: z.literal(2),
|
|
77
|
+
identityAnchors: identityAnchorsSchema
|
|
78
|
+
}).strict();
|
|
79
|
+
function enrichmentInputSha256(value) {
|
|
80
|
+
return sha256(canonicalJson(enrichmentInputSchema.parse({
|
|
81
|
+
displayName: value.displayName,
|
|
82
|
+
emails: value.emails,
|
|
83
|
+
phones: value.phones,
|
|
84
|
+
organization: value.organization,
|
|
85
|
+
title: value.title,
|
|
86
|
+
sources: value.sources,
|
|
87
|
+
providerHandles: value.providerHandles,
|
|
88
|
+
enrichmentInputVersion: value.enrichmentInputVersion,
|
|
89
|
+
identityAnchors: value.identityAnchors
|
|
90
|
+
})));
|
|
91
|
+
}
|
|
92
|
+
function legacyEnrichmentInputSha256(value) {
|
|
93
|
+
return sha256(canonicalJson(legacyEnrichmentInputSchema.parse({
|
|
94
|
+
displayName: value.displayName,
|
|
95
|
+
emails: value.emails,
|
|
96
|
+
phones: value.phones,
|
|
97
|
+
organization: value.organization,
|
|
98
|
+
title: value.title,
|
|
99
|
+
sources: value.sources,
|
|
100
|
+
providerHandles: value.providerHandles
|
|
101
|
+
})));
|
|
102
|
+
}
|
|
103
|
+
var legacyV1CloudContactSchema = z.object({
|
|
104
|
+
id: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
105
|
+
localPersonId: z.string().regex(/^[1-9][0-9]{0,18}$/u),
|
|
106
|
+
displayName: boundedText(1024),
|
|
107
|
+
emails: z.array(cloudEmailSchema).max(100),
|
|
108
|
+
phones: z.array(boundedText(256)).max(100),
|
|
109
|
+
organization: nullableText(1024),
|
|
110
|
+
title: nullableText(1024),
|
|
111
|
+
birthday: birthdaySchema,
|
|
112
|
+
sources: z.array(sourceLabelSchema).max(16),
|
|
113
|
+
providerHandles: z.record(boundedText(128), boundedText(2048)).refine((value) => Object.keys(value).length <= 50, "providerHandles contains too many entries"),
|
|
114
|
+
interactionCount: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
115
|
+
reciprocal: z.boolean(),
|
|
116
|
+
firstInteractionAt: z.iso.datetime({ offset: true }).nullable(),
|
|
117
|
+
lastInteractionAt: z.iso.datetime({ offset: true }).nullable(),
|
|
118
|
+
updatedAt: z.iso.datetime({ offset: true }),
|
|
119
|
+
metadataSha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
120
|
+
enrichmentInputSha256: z.string().regex(/^[a-f0-9]{64}$/u)
|
|
121
|
+
}).strict();
|
|
122
|
+
var legacyV0CloudContactSchema = legacyV1CloudContactSchema.omit({ enrichmentInputSha256: true }).strict();
|
|
123
|
+
var legacyV2CloudContactSchema = legacyV1CloudContactSchema.extend({
|
|
124
|
+
enrichmentInputVersion: z.literal(2),
|
|
125
|
+
identityAnchors: identityAnchorsSchema
|
|
126
|
+
}).strict().superRefine((contact, context) => {
|
|
127
|
+
if (contact.identityAnchors.state !== "available") {
|
|
128
|
+
context.addIssue({
|
|
129
|
+
code: "custom",
|
|
130
|
+
path: ["identityAnchors", "state"],
|
|
131
|
+
message: "Current sync contacts require available identity anchors"
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const emails = new Set(contact.emails);
|
|
136
|
+
const phones = new Set(contact.phones);
|
|
137
|
+
contact.identityAnchors.emails.forEach((email, index) => {
|
|
138
|
+
if (!emails.has(email))
|
|
139
|
+
context.addIssue({
|
|
140
|
+
code: "custom",
|
|
141
|
+
path: ["identityAnchors", "emails", index],
|
|
142
|
+
message: "Identity email anchor must also be an observed email"
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
contact.identityAnchors.phones.forEach((phone, index) => {
|
|
146
|
+
if (!phones.has(phone))
|
|
147
|
+
context.addIssue({
|
|
148
|
+
code: "custom",
|
|
149
|
+
path: ["identityAnchors", "phones", index],
|
|
150
|
+
message: "Identity phone anchor must also be an observed phone"
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
var cloudContactSchema = legacyV1CloudContactSchema.extend({
|
|
155
|
+
enrichmentInputVersion: z.literal(2),
|
|
156
|
+
identityAnchors: identityAnchorsSchema,
|
|
157
|
+
automaticEnrichmentSelection: automaticEnrichmentSelectionSchema
|
|
158
|
+
}).strict().superRefine((contact, context) => {
|
|
159
|
+
if (contact.identityAnchors.state !== "available") {
|
|
160
|
+
context.addIssue({
|
|
161
|
+
code: "custom",
|
|
162
|
+
path: ["identityAnchors", "state"],
|
|
163
|
+
message: "Current sync contacts require available identity anchors"
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const emails = new Set(contact.emails);
|
|
168
|
+
const phones = new Set(contact.phones);
|
|
169
|
+
contact.identityAnchors.emails.forEach((email, index) => {
|
|
170
|
+
if (!emails.has(email))
|
|
171
|
+
context.addIssue({
|
|
172
|
+
code: "custom",
|
|
173
|
+
path: ["identityAnchors", "emails", index],
|
|
174
|
+
message: "Identity email anchor must also be an observed email"
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
contact.identityAnchors.phones.forEach((phone, index) => {
|
|
178
|
+
if (!phones.has(phone))
|
|
179
|
+
context.addIssue({
|
|
180
|
+
code: "custom",
|
|
181
|
+
path: ["identityAnchors", "phones", index],
|
|
182
|
+
message: "Identity phone anchor must also be an observed phone"
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
var quarantinedCloudContactSchema = legacyV1CloudContactSchema.extend({
|
|
187
|
+
enrichmentInputVersion: z.literal(2),
|
|
188
|
+
identityAnchors: z.object({ state: z.literal("unavailable") }).strict(),
|
|
189
|
+
automaticEnrichmentSelection: unavailableAutomaticEnrichmentSelectionSchema
|
|
190
|
+
}).strict();
|
|
191
|
+
var selectionQuarantinedCloudContactSchema = legacyV1CloudContactSchema.extend({
|
|
192
|
+
enrichmentInputVersion: z.literal(2),
|
|
193
|
+
identityAnchors: identityAnchorsSchema,
|
|
194
|
+
automaticEnrichmentSelection: unavailableAutomaticEnrichmentSelectionSchema
|
|
195
|
+
}).strict().superRefine((contact, context) => {
|
|
196
|
+
if (contact.identityAnchors.state !== "available") {
|
|
197
|
+
context.addIssue({
|
|
198
|
+
code: "custom",
|
|
199
|
+
path: ["identityAnchors", "state"],
|
|
200
|
+
message: "Current sync contacts require available identity anchors"
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const emails = new Set(contact.emails);
|
|
205
|
+
const phones = new Set(contact.phones);
|
|
206
|
+
contact.identityAnchors.emails.forEach((email, index) => {
|
|
207
|
+
if (!emails.has(email))
|
|
208
|
+
context.addIssue({
|
|
209
|
+
code: "custom",
|
|
210
|
+
path: ["identityAnchors", "emails", index],
|
|
211
|
+
message: "Identity email anchor must also be an observed email"
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
contact.identityAnchors.phones.forEach((phone, index) => {
|
|
215
|
+
if (!phones.has(phone))
|
|
216
|
+
context.addIssue({
|
|
217
|
+
code: "custom",
|
|
218
|
+
path: ["identityAnchors", "phones", index],
|
|
219
|
+
message: "Identity phone anchor must also be an observed phone"
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
var syncStartSchema = z.object({
|
|
224
|
+
contactCount: z.number().int().min(0).max(1e6),
|
|
225
|
+
databaseFingerprint: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
226
|
+
schemaVersion: z.number().int().min(1).max(1000),
|
|
227
|
+
resumeVersion: z.literal(1).optional()
|
|
228
|
+
}).strict();
|
|
229
|
+
var syncPageSchema = z.object({
|
|
230
|
+
snapshotId: z.uuid(),
|
|
231
|
+
ordinal: z.number().int().min(0).max(1e5),
|
|
232
|
+
contacts: z.array(cloudContactSchema).min(1).max(200)
|
|
233
|
+
}).strict();
|
|
234
|
+
function syncContactInputVersion(contact) {
|
|
235
|
+
if ("enrichmentInputVersion" in contact && contact.enrichmentInputVersion === 2)
|
|
236
|
+
return 2;
|
|
237
|
+
return "enrichmentInputSha256" in contact && typeof contact.enrichmentInputSha256 === "string" ? 1 : 0;
|
|
238
|
+
}
|
|
239
|
+
function syncSelectionVersion(contact) {
|
|
240
|
+
return "automaticEnrichmentSelection" in contact ? 1 : 0;
|
|
241
|
+
}
|
|
242
|
+
var serverSyncPageInputSchema = z.object({
|
|
243
|
+
snapshotId: z.uuid(),
|
|
244
|
+
ordinal: z.number().int().min(0).max(1e5),
|
|
245
|
+
contacts: z.array(z.union([
|
|
246
|
+
cloudContactSchema,
|
|
247
|
+
legacyV2CloudContactSchema,
|
|
248
|
+
legacyV1CloudContactSchema,
|
|
249
|
+
legacyV0CloudContactSchema
|
|
250
|
+
])).min(1).max(200)
|
|
251
|
+
}).strict().superRefine((input, context) => {
|
|
252
|
+
const coordinateVersions = new Set(input.contacts.map((contact) => `${syncContactInputVersion(contact)}:${syncSelectionVersion(contact)}`));
|
|
253
|
+
if (coordinateVersions.size !== 1) {
|
|
254
|
+
context.addIssue({
|
|
255
|
+
code: "custom",
|
|
256
|
+
path: ["contacts"],
|
|
257
|
+
message: "Sync page contacts must use one projection version."
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
input.contacts.forEach((contact, index) => {
|
|
261
|
+
const version = syncContactInputVersion(contact);
|
|
262
|
+
if (version === 0)
|
|
263
|
+
return;
|
|
264
|
+
const expected = version === 2 ? enrichmentInputSha256(contact) : legacyEnrichmentInputSha256(contact);
|
|
265
|
+
const actual = "enrichmentInputSha256" in contact ? contact.enrichmentInputSha256 : null;
|
|
266
|
+
if (actual !== expected)
|
|
267
|
+
context.addIssue({
|
|
268
|
+
code: "custom",
|
|
269
|
+
path: ["contacts", index, "enrichmentInputSha256"],
|
|
270
|
+
message: `Sync contact enrichment input hash is invalid for version ${version}`
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
var serverSyncPageSchema = serverSyncPageInputSchema.transform((input) => {
|
|
275
|
+
const inputVersion = syncContactInputVersion(input.contacts[0]);
|
|
276
|
+
const selectionVersion = syncSelectionVersion(input.contacts[0]);
|
|
277
|
+
return {
|
|
278
|
+
snapshotId: input.snapshotId,
|
|
279
|
+
ordinal: input.ordinal,
|
|
280
|
+
inputVersion,
|
|
281
|
+
selectionVersion,
|
|
282
|
+
legacyPayloadSha256: inputVersion === 2 && selectionVersion === 1 ? null : sha256(canonicalJson(input.contacts)),
|
|
283
|
+
contacts: input.contacts.map((contact) => {
|
|
284
|
+
if (inputVersion === 2 && selectionVersion === 1)
|
|
285
|
+
return cloudContactSchema.parse(contact);
|
|
286
|
+
if (inputVersion === 2)
|
|
287
|
+
return selectionQuarantinedCloudContactSchema.parse({
|
|
288
|
+
...contact,
|
|
289
|
+
automaticEnrichmentSelection: { version: 0, state: "unavailable" }
|
|
290
|
+
});
|
|
291
|
+
const upgraded = {
|
|
292
|
+
...contact,
|
|
293
|
+
enrichmentInputVersion: 2,
|
|
294
|
+
identityAnchors: { state: "unavailable" },
|
|
295
|
+
automaticEnrichmentSelection: { version: 0, state: "unavailable" }
|
|
296
|
+
};
|
|
297
|
+
return quarantinedCloudContactSchema.parse({
|
|
298
|
+
...upgraded,
|
|
299
|
+
enrichmentInputSha256: enrichmentInputSha256(upgraded)
|
|
300
|
+
});
|
|
301
|
+
})
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
var syncFinishSchema = z.object({
|
|
305
|
+
snapshotId: z.uuid(),
|
|
306
|
+
pages: z.number().int().min(0).max(1e5),
|
|
307
|
+
contactCount: z.number().int().min(0).max(1e6)
|
|
308
|
+
}).strict();
|
|
309
|
+
var cliEnrichmentContactSchema = z.object({
|
|
310
|
+
localPersonId: z.string().regex(/^[1-9][0-9]{0,18}$/u),
|
|
311
|
+
enrichmentInputVersion: z.literal(2),
|
|
312
|
+
enrichmentInputSha256: z.string().regex(/^[a-f0-9]{64}$/u)
|
|
313
|
+
}).strict();
|
|
314
|
+
var legacyV1CliEnrichmentContactSchema = z.object({
|
|
315
|
+
localPersonId: z.string().regex(/^[1-9][0-9]{0,18}$/u),
|
|
316
|
+
enrichmentInputSha256: z.string().regex(/^[a-f0-9]{64}$/u)
|
|
317
|
+
}).strict();
|
|
318
|
+
var legacyV0CliEnrichmentContactSchema = z.object({
|
|
319
|
+
localPersonId: z.string().regex(/^[1-9][0-9]{0,18}$/u),
|
|
320
|
+
metadataSha256: z.string().regex(/^[a-f0-9]{64}$/u)
|
|
321
|
+
}).strict();
|
|
322
|
+
var serverCliEnrichmentContactSchema = z.union([
|
|
323
|
+
cliEnrichmentContactSchema,
|
|
324
|
+
legacyV1CliEnrichmentContactSchema,
|
|
325
|
+
legacyV0CliEnrichmentContactSchema
|
|
326
|
+
]);
|
|
327
|
+
function cliEnrichmentInputVersion(contact) {
|
|
328
|
+
if ("enrichmentInputVersion" in contact)
|
|
329
|
+
return 2;
|
|
330
|
+
return "enrichmentInputSha256" in contact ? 1 : 0;
|
|
331
|
+
}
|
|
332
|
+
var serverCliEnrichmentContactsSchema = z.array(serverCliEnrichmentContactSchema).min(1).max(100).refine((contacts) => new Set(contacts.map(cliEnrichmentInputVersion)).size === 1, "A CLI enrichment request must use one hash-coordinate version.");
|
|
333
|
+
var cliEnrichmentPreviewSchema = z.object({
|
|
334
|
+
contacts: z.array(cliEnrichmentContactSchema).min(1).max(100)
|
|
335
|
+
}).strict();
|
|
336
|
+
var cliPrioritizedEnrichmentPreviewSchema = z.object({
|
|
337
|
+
requestedCount: z.number().int().min(1).max(100),
|
|
338
|
+
priorityPolicyVersion: z.literal(enrichmentPriorityPolicyVersion)
|
|
339
|
+
}).strict();
|
|
340
|
+
var serverCliEnrichmentPreviewSchema = z.object({
|
|
341
|
+
contacts: serverCliEnrichmentContactsSchema
|
|
342
|
+
}).strict();
|
|
343
|
+
var cliEnrichmentConfirmationTokenSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/u);
|
|
344
|
+
var cliEnrichmentStartSchema = z.object({
|
|
345
|
+
previewId: z.uuid(),
|
|
346
|
+
confirmationToken: cliEnrichmentConfirmationTokenSchema,
|
|
347
|
+
contacts: z.array(cliEnrichmentContactSchema).min(1).max(100)
|
|
348
|
+
}).strict();
|
|
349
|
+
var serverCliEnrichmentStartSchema = z.object({
|
|
350
|
+
previewId: z.uuid(),
|
|
351
|
+
confirmationToken: cliEnrichmentConfirmationTokenSchema,
|
|
352
|
+
contacts: serverCliEnrichmentContactsSchema
|
|
353
|
+
}).strict();
|
|
354
|
+
var cliEnrichmentStatusSchema = z.object({
|
|
355
|
+
jobId: z.uuid()
|
|
356
|
+
}).strict();
|
|
357
|
+
var cliEnrichmentPublicEmailsRequestSchema = z.object({}).strict();
|
|
358
|
+
var cliEnrichmentRevalidationSchema = z.object({
|
|
359
|
+
limit: z.number().int().min(1).max(100)
|
|
360
|
+
}).strict();
|
|
361
|
+
var cliEnrichmentRevalidationResponseSchema = z.object({
|
|
362
|
+
processed: z.number().int().min(0).max(100),
|
|
363
|
+
accepted: z.number().int().min(0).max(100),
|
|
364
|
+
rejected: z.number().int().min(0).max(100),
|
|
365
|
+
hasMore: z.boolean(),
|
|
366
|
+
targetPolicyVersion: z.literal(enrichmentPolicyVersion),
|
|
367
|
+
validationProfileSha256: z.string().regex(/^[a-f0-9]{64}$/u)
|
|
368
|
+
}).strict().superRefine((value, context) => {
|
|
369
|
+
if (value.accepted + value.rejected !== value.processed) {
|
|
370
|
+
context.addIssue({
|
|
371
|
+
code: "custom",
|
|
372
|
+
message: "Accepted and rejected counts must equal the processed count.",
|
|
373
|
+
path: ["processed"]
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
var deviceStartSchema = z.object({
|
|
378
|
+
deviceName: boundedText(128),
|
|
379
|
+
tokenSha256: z.string().regex(/^[a-f0-9]{64}$/u)
|
|
380
|
+
}).strict();
|
|
381
|
+
var deviceCodeSchema = z.string().regex(/^[A-Za-z0-9_-]{64}$/u);
|
|
382
|
+
var deviceStatusSchema = z.object({
|
|
383
|
+
deviceCode: deviceCodeSchema
|
|
384
|
+
}).strict();
|
|
385
|
+
var cloudDeviceIdSchema = z.uuid();
|
|
386
|
+
var cloudDevicesRequestSchema = z.object({}).strict();
|
|
387
|
+
var cloudDeviceRevokeRequestSchema = z.object({
|
|
388
|
+
deviceId: cloudDeviceIdSchema
|
|
389
|
+
}).strict();
|
|
390
|
+
var cloudDeviceSchema = z.object({
|
|
391
|
+
id: cloudDeviceIdSchema,
|
|
392
|
+
name: boundedText(128),
|
|
393
|
+
createdAt: z.iso.datetime({ offset: true }),
|
|
394
|
+
lastSeenAt: z.iso.datetime({ offset: true }),
|
|
395
|
+
revokedAt: z.iso.datetime({ offset: true }).nullable(),
|
|
396
|
+
current: z.boolean()
|
|
397
|
+
}).strict();
|
|
398
|
+
var cloudDevicesResponseSchema = z.object({
|
|
399
|
+
devices: z.array(cloudDeviceSchema).max(200)
|
|
400
|
+
}).strict();
|
|
401
|
+
var cloudDeviceRevokeResponseSchema = z.object({
|
|
402
|
+
deviceId: cloudDeviceIdSchema,
|
|
403
|
+
revokedAt: z.iso.datetime({ offset: true })
|
|
404
|
+
}).strict();
|
|
405
|
+
var enrichmentClaimSchema = z.object({
|
|
406
|
+
field: z.enum(["headline", "organization", "role", "location", "website", "publicEmail"]),
|
|
407
|
+
evidenceIndexes: z.array(z.number().int().min(0).max(4)).min(1).max(5)
|
|
408
|
+
}).strict();
|
|
409
|
+
var enrichmentOutputSchema = z.object({
|
|
410
|
+
identityMatch: z.enum(["confirmed", "possible", "insufficient"]),
|
|
411
|
+
identityEvidenceIndexes: z.array(z.number().int().min(0).max(4)).max(5),
|
|
412
|
+
confidence: z.number().int().min(0).max(100),
|
|
413
|
+
headline: nullableText(1024),
|
|
414
|
+
organization: nullableText(1024),
|
|
415
|
+
role: nullableText(1024),
|
|
416
|
+
location: nullableText(1024),
|
|
417
|
+
website: httpUrlSchema.nullable(),
|
|
418
|
+
publicEmail: z.email().max(1024).nullable(),
|
|
419
|
+
publicEmailEvidenceIndex: z.number().int().min(0).max(4).nullable(),
|
|
420
|
+
notes: z.string().trim().max(2000),
|
|
421
|
+
claims: z.array(enrichmentClaimSchema).max(24)
|
|
422
|
+
}).strict();
|
|
423
|
+
var enrichmentEvidenceReferenceSchema = z.object({
|
|
424
|
+
url: httpUrlSchema,
|
|
425
|
+
title: boundedText(1024),
|
|
426
|
+
excerpt: boundedText(4000),
|
|
427
|
+
kind: z.enum(["public_page", "provider_attestation"]).optional()
|
|
428
|
+
}).strict();
|
|
429
|
+
var enrichmentIdentitySubjectSchema = enrichmentInputSchema.pick({
|
|
430
|
+
displayName: true,
|
|
431
|
+
emails: true,
|
|
432
|
+
phones: true,
|
|
433
|
+
identityAnchors: true,
|
|
434
|
+
organization: true,
|
|
435
|
+
title: true,
|
|
436
|
+
providerHandles: true
|
|
437
|
+
}).superRefine((subject, context) => {
|
|
438
|
+
if (subject.identityAnchors.state !== "available")
|
|
439
|
+
return;
|
|
440
|
+
const emails = new Set(subject.emails);
|
|
441
|
+
const phones = new Set(subject.phones);
|
|
442
|
+
subject.identityAnchors.emails.forEach((email, index) => {
|
|
443
|
+
if (!emails.has(email))
|
|
444
|
+
context.addIssue({
|
|
445
|
+
code: "custom",
|
|
446
|
+
path: ["identityAnchors", "emails", index],
|
|
447
|
+
message: "Identity email anchor must also be an observed email"
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
subject.identityAnchors.phones.forEach((phone, index) => {
|
|
451
|
+
if (!phones.has(phone))
|
|
452
|
+
context.addIssue({
|
|
453
|
+
code: "custom",
|
|
454
|
+
path: ["identityAnchors", "phones", index],
|
|
455
|
+
message: "Identity phone anchor must also be an observed phone"
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
var enrichmentEvidenceRecordSchema = enrichmentEvidenceReferenceSchema.extend({
|
|
460
|
+
sourceId: boundedText(2048)
|
|
461
|
+
}).strict();
|
|
462
|
+
var publicEvidenceSchema = enrichmentEvidenceReferenceSchema.omit({ kind: true }).strict();
|
|
463
|
+
var manualResearchSchema = enrichmentOutputSchema.extend({
|
|
464
|
+
evidence: z.array(publicEvidenceSchema).max(5)
|
|
465
|
+
}).strict();
|
|
466
|
+
function normalizedEvidenceText(value) {
|
|
467
|
+
return value.normalize("NFKC").toLocaleLowerCase("en-US").replaceAll(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
468
|
+
}
|
|
469
|
+
var LATIN_IDENTITY_FOLDABLE_MARKS = new Set([
|
|
470
|
+
..."\u0300\u0301\u0302\u0303\u0304\u0306\u0307\u0308\u0309\u030A\u030B\u030C\u030F\u0311\u031B" + "\u0323\u0324\u0325\u0326\u0327\u0328\u032D\u032E\u0330\u0331"
|
|
471
|
+
]);
|
|
472
|
+
var HISTORICAL_PROFESSIONAL_MARKERS = new Set([
|
|
473
|
+
"alum",
|
|
474
|
+
"alumni",
|
|
475
|
+
"ceased",
|
|
476
|
+
"departed",
|
|
477
|
+
"down",
|
|
478
|
+
"emeritus",
|
|
479
|
+
"ended",
|
|
480
|
+
"ex",
|
|
481
|
+
"exited",
|
|
482
|
+
"former",
|
|
483
|
+
"formerly",
|
|
484
|
+
"left",
|
|
485
|
+
"past",
|
|
486
|
+
"previous",
|
|
487
|
+
"previously",
|
|
488
|
+
"quit",
|
|
489
|
+
"resigned",
|
|
490
|
+
"retired",
|
|
491
|
+
"sold",
|
|
492
|
+
"stepped",
|
|
493
|
+
"switched",
|
|
494
|
+
"until",
|
|
495
|
+
"was",
|
|
496
|
+
"were"
|
|
497
|
+
]);
|
|
498
|
+
var CURRENT_PROFESSIONAL_MARKERS = new Set([
|
|
499
|
+
"are",
|
|
500
|
+
"current",
|
|
501
|
+
"currently",
|
|
502
|
+
"is",
|
|
503
|
+
"now",
|
|
504
|
+
"present",
|
|
505
|
+
"presently",
|
|
506
|
+
"serves",
|
|
507
|
+
"works"
|
|
508
|
+
]);
|
|
509
|
+
var RESIDENCE_LOCATION_MARKERS = new Set([
|
|
510
|
+
"base",
|
|
511
|
+
"based",
|
|
512
|
+
"home",
|
|
513
|
+
"lives",
|
|
514
|
+
"located",
|
|
515
|
+
"location",
|
|
516
|
+
"residence",
|
|
517
|
+
"resident",
|
|
518
|
+
"resides"
|
|
519
|
+
]);
|
|
520
|
+
var EVENT_LOCATION_MARKERS = new Set([
|
|
521
|
+
"conference",
|
|
522
|
+
"conferences",
|
|
523
|
+
"event",
|
|
524
|
+
"events",
|
|
525
|
+
"keynote",
|
|
526
|
+
"keynotes",
|
|
527
|
+
"meetup",
|
|
528
|
+
"meetups",
|
|
529
|
+
"panel",
|
|
530
|
+
"panels",
|
|
531
|
+
"presented",
|
|
532
|
+
"presenting",
|
|
533
|
+
"presentation",
|
|
534
|
+
"speaker",
|
|
535
|
+
"speakers",
|
|
536
|
+
"speaking",
|
|
537
|
+
"spoke",
|
|
538
|
+
"summit",
|
|
539
|
+
"summits",
|
|
540
|
+
"talk",
|
|
541
|
+
"talks",
|
|
542
|
+
"workshop",
|
|
543
|
+
"workshops"
|
|
544
|
+
]);
|
|
545
|
+
var NON_PERSON_LOCATION_MARKERS = new Set([
|
|
546
|
+
"branch",
|
|
547
|
+
"branches",
|
|
548
|
+
"business",
|
|
549
|
+
"businesses",
|
|
550
|
+
"company",
|
|
551
|
+
"companies",
|
|
552
|
+
"employer",
|
|
553
|
+
"headquarters",
|
|
554
|
+
"hq",
|
|
555
|
+
"office",
|
|
556
|
+
"offices",
|
|
557
|
+
"organization",
|
|
558
|
+
"organisations",
|
|
559
|
+
"organizations",
|
|
560
|
+
"startup",
|
|
561
|
+
"startups",
|
|
562
|
+
"team",
|
|
563
|
+
"teams",
|
|
564
|
+
"venue",
|
|
565
|
+
"venues"
|
|
566
|
+
]);
|
|
567
|
+
var HISTORICAL_LOCATION_MARKERS = new Set([
|
|
568
|
+
"ex",
|
|
569
|
+
"former",
|
|
570
|
+
"formerly",
|
|
571
|
+
"once",
|
|
572
|
+
"past",
|
|
573
|
+
"previous",
|
|
574
|
+
"previously",
|
|
575
|
+
"used",
|
|
576
|
+
"was",
|
|
577
|
+
"were"
|
|
578
|
+
]);
|
|
579
|
+
var LOCATION_COPULA_AND_MODIFIERS = new Set([
|
|
580
|
+
"are",
|
|
581
|
+
"current",
|
|
582
|
+
"currently",
|
|
583
|
+
"is",
|
|
584
|
+
"now",
|
|
585
|
+
"presently"
|
|
586
|
+
]);
|
|
587
|
+
var PERSON_LOCATION_PRONOUNS = new Set(["he", "she", "they"]);
|
|
588
|
+
var LOCATION_RELATION_GLUE = new Set(["at", "for", "in", "is", "of", "to"]);
|
|
589
|
+
var LOCATION_SUPERSESSION_MARKERS = new Set([
|
|
590
|
+
"departed",
|
|
591
|
+
"left",
|
|
592
|
+
"moved",
|
|
593
|
+
"relocated",
|
|
594
|
+
"relocating",
|
|
595
|
+
"vacated"
|
|
596
|
+
]);
|
|
597
|
+
var PROFESSIONAL_EVENT_MARKERS = new Set([
|
|
598
|
+
"attended",
|
|
599
|
+
"attending",
|
|
600
|
+
"conference",
|
|
601
|
+
"dinner",
|
|
602
|
+
"event",
|
|
603
|
+
"game",
|
|
604
|
+
"guest",
|
|
605
|
+
"hosted",
|
|
606
|
+
"interviewed",
|
|
607
|
+
"keynote",
|
|
608
|
+
"lunch",
|
|
609
|
+
"meet",
|
|
610
|
+
"meeting",
|
|
611
|
+
"met",
|
|
612
|
+
"panel",
|
|
613
|
+
"presented",
|
|
614
|
+
"presenting",
|
|
615
|
+
"speaker",
|
|
616
|
+
"speaking",
|
|
617
|
+
"spoke",
|
|
618
|
+
"summit",
|
|
619
|
+
"talk",
|
|
620
|
+
"venue",
|
|
621
|
+
"visited",
|
|
622
|
+
"visiting",
|
|
623
|
+
"webinar",
|
|
624
|
+
"workshop"
|
|
625
|
+
]);
|
|
626
|
+
var NON_EMPLOYMENT_RELATION_MARKERS = new Set([
|
|
627
|
+
"candidate",
|
|
628
|
+
"client",
|
|
629
|
+
"competitor",
|
|
630
|
+
"critic",
|
|
631
|
+
"criticized",
|
|
632
|
+
"customer",
|
|
633
|
+
"grant",
|
|
634
|
+
"grantee",
|
|
635
|
+
"invested",
|
|
636
|
+
"investor",
|
|
637
|
+
"invests",
|
|
638
|
+
"received",
|
|
639
|
+
"sponsor",
|
|
640
|
+
"sponsored",
|
|
641
|
+
"vendor"
|
|
642
|
+
]);
|
|
643
|
+
var SIMPLE_AFFILIATION_GLUE = new Set([
|
|
644
|
+
"a",
|
|
645
|
+
"an",
|
|
646
|
+
"as",
|
|
647
|
+
"at",
|
|
648
|
+
"current",
|
|
649
|
+
"currently",
|
|
650
|
+
"is",
|
|
651
|
+
"the"
|
|
652
|
+
]);
|
|
653
|
+
var PROFESSIONAL_ROLE_MARKERS = new Set([
|
|
654
|
+
"chief",
|
|
655
|
+
"cofounder",
|
|
656
|
+
"director",
|
|
657
|
+
"employee",
|
|
658
|
+
"founder",
|
|
659
|
+
"lead",
|
|
660
|
+
"leader",
|
|
661
|
+
"member",
|
|
662
|
+
"owner",
|
|
663
|
+
"partner",
|
|
664
|
+
"president",
|
|
665
|
+
"principal"
|
|
666
|
+
]);
|
|
667
|
+
var PROFESSIONAL_RELATION_BOUNDARIES = new Set(["and", "but", "then", "while"]);
|
|
668
|
+
var PROFESSIONAL_RELATION_MODIFIERS = new Set(["current", "currently", "now", "presently"]);
|
|
669
|
+
var ORGANIZATION_SUFFIX_DISQUALIFIERS = new Set([
|
|
670
|
+
"alum",
|
|
671
|
+
"alumni",
|
|
672
|
+
"candidate",
|
|
673
|
+
"client",
|
|
674
|
+
"competitor",
|
|
675
|
+
"critic",
|
|
676
|
+
"customer",
|
|
677
|
+
"guest",
|
|
678
|
+
"investee",
|
|
679
|
+
"investor",
|
|
680
|
+
"sponsor",
|
|
681
|
+
"vendor"
|
|
682
|
+
]);
|
|
683
|
+
var ROLE_SUFFIX_DISQUALIFIERS = new Set([
|
|
684
|
+
"alum",
|
|
685
|
+
"alumni",
|
|
686
|
+
"aspirant",
|
|
687
|
+
"candidate",
|
|
688
|
+
"emeritus",
|
|
689
|
+
"impersonator",
|
|
690
|
+
"nominee",
|
|
691
|
+
"pretender"
|
|
692
|
+
]);
|
|
693
|
+
var PROFESSIONAL_SUBJECT_PRONOUNS = new Set(["he", "her", "his", "she", "their", "they"]);
|
|
694
|
+
var CURRENT_PROFESSIONAL_CLAUSE_MARKERS = new Set([
|
|
695
|
+
"appointed",
|
|
696
|
+
"are",
|
|
697
|
+
"employed",
|
|
698
|
+
"founded",
|
|
699
|
+
"is",
|
|
700
|
+
"joined",
|
|
701
|
+
"leads",
|
|
702
|
+
"named",
|
|
703
|
+
"owns",
|
|
704
|
+
"serves",
|
|
705
|
+
"serving",
|
|
706
|
+
"works",
|
|
707
|
+
"working"
|
|
708
|
+
]);
|
|
709
|
+
var ROLE_EMAIL_LOCAL_PARTS = new Set([
|
|
710
|
+
"abuse",
|
|
711
|
+
"accounts",
|
|
712
|
+
"admin",
|
|
713
|
+
"admissions",
|
|
714
|
+
"assistant",
|
|
715
|
+
"billing",
|
|
716
|
+
"careers",
|
|
717
|
+
"community",
|
|
718
|
+
"connect",
|
|
719
|
+
"contact",
|
|
720
|
+
"contactus",
|
|
721
|
+
"customerservice",
|
|
722
|
+
"events",
|
|
723
|
+
"finance",
|
|
724
|
+
"general",
|
|
725
|
+
"hello",
|
|
726
|
+
"help",
|
|
727
|
+
"hr",
|
|
728
|
+
"info",
|
|
729
|
+
"inquiries",
|
|
730
|
+
"investors",
|
|
731
|
+
"jobs",
|
|
732
|
+
"legal",
|
|
733
|
+
"mail",
|
|
734
|
+
"marketing",
|
|
735
|
+
"media",
|
|
736
|
+
"newsletter",
|
|
737
|
+
"noreply",
|
|
738
|
+
"office",
|
|
739
|
+
"operations",
|
|
740
|
+
"ops",
|
|
741
|
+
"partners",
|
|
742
|
+
"postmaster",
|
|
743
|
+
"press",
|
|
744
|
+
"privacy",
|
|
745
|
+
"reception",
|
|
746
|
+
"sales",
|
|
747
|
+
"security",
|
|
748
|
+
"support",
|
|
749
|
+
"team",
|
|
750
|
+
"webmaster"
|
|
751
|
+
]);
|
|
752
|
+
var PROFILE_IDENTITY_LINK_NEGATIONS = new Set([
|
|
753
|
+
"aint",
|
|
754
|
+
"alleged",
|
|
755
|
+
"allegedly",
|
|
756
|
+
"arent",
|
|
757
|
+
"cannot",
|
|
758
|
+
"cant",
|
|
759
|
+
"couldnt",
|
|
760
|
+
"criticized",
|
|
761
|
+
"denied",
|
|
762
|
+
"denies",
|
|
763
|
+
"different",
|
|
764
|
+
"disassociated",
|
|
765
|
+
"disavowed",
|
|
766
|
+
"disavows",
|
|
767
|
+
"disclaimed",
|
|
768
|
+
"disclaims",
|
|
769
|
+
"didnt",
|
|
770
|
+
"doesnt",
|
|
771
|
+
"dont",
|
|
772
|
+
"fake",
|
|
773
|
+
"fan",
|
|
774
|
+
"fraudulent",
|
|
775
|
+
"hadnt",
|
|
776
|
+
"hasnt",
|
|
777
|
+
"havent",
|
|
778
|
+
"impersonator",
|
|
779
|
+
"incorrect",
|
|
780
|
+
"isnt",
|
|
781
|
+
"lacked",
|
|
782
|
+
"lacking",
|
|
783
|
+
"lacks",
|
|
784
|
+
"mirror",
|
|
785
|
+
"misattributed",
|
|
786
|
+
"mistaken",
|
|
787
|
+
"neither",
|
|
788
|
+
"never",
|
|
789
|
+
"no",
|
|
790
|
+
"nor",
|
|
791
|
+
"not",
|
|
792
|
+
"parody",
|
|
793
|
+
"purported",
|
|
794
|
+
"satire",
|
|
795
|
+
"shant",
|
|
796
|
+
"shouldnt",
|
|
797
|
+
"supposed",
|
|
798
|
+
"unaffiliated",
|
|
799
|
+
"unassociated",
|
|
800
|
+
"unconnected",
|
|
801
|
+
"unofficial",
|
|
802
|
+
"unrelated",
|
|
803
|
+
"wasnt",
|
|
804
|
+
"werent",
|
|
805
|
+
"without",
|
|
806
|
+
"wont",
|
|
807
|
+
"wouldnt",
|
|
808
|
+
"wrong",
|
|
809
|
+
"zero"
|
|
810
|
+
]);
|
|
811
|
+
var PROFILE_IDENTITY_NEGATED_AUXILIARY_STEMS = new Set([
|
|
812
|
+
"ain",
|
|
813
|
+
"aren",
|
|
814
|
+
"can",
|
|
815
|
+
"couldn",
|
|
816
|
+
"didn",
|
|
817
|
+
"doesn",
|
|
818
|
+
"don",
|
|
819
|
+
"hadn",
|
|
820
|
+
"hasn",
|
|
821
|
+
"haven",
|
|
822
|
+
"isn",
|
|
823
|
+
"mightn",
|
|
824
|
+
"mustn",
|
|
825
|
+
"needn",
|
|
826
|
+
"shan",
|
|
827
|
+
"shouldn",
|
|
828
|
+
"wasn",
|
|
829
|
+
"weren",
|
|
830
|
+
"won",
|
|
831
|
+
"wouldn"
|
|
832
|
+
]);
|
|
833
|
+
var PROFILE_IDENTITY_CURRENT_PREFIX_TOKENS = new Set([
|
|
834
|
+
"a",
|
|
835
|
+
"an",
|
|
836
|
+
"current",
|
|
837
|
+
"currently",
|
|
838
|
+
"now",
|
|
839
|
+
"present",
|
|
840
|
+
"presently",
|
|
841
|
+
"the"
|
|
842
|
+
]);
|
|
843
|
+
var PROFILE_IDENTITY_PROFESSIONAL_ROLE_TOKENS = new Set([
|
|
844
|
+
"advisor",
|
|
845
|
+
"analyst",
|
|
846
|
+
"architect",
|
|
847
|
+
"attorney",
|
|
848
|
+
"ceo",
|
|
849
|
+
"cfo",
|
|
850
|
+
"chief",
|
|
851
|
+
"cofounder",
|
|
852
|
+
"consultant",
|
|
853
|
+
"cto",
|
|
854
|
+
"designer",
|
|
855
|
+
"developer",
|
|
856
|
+
"director",
|
|
857
|
+
"employee",
|
|
858
|
+
"engineer",
|
|
859
|
+
"executive",
|
|
860
|
+
"founder",
|
|
861
|
+
"investor",
|
|
862
|
+
"lead",
|
|
863
|
+
"leader",
|
|
864
|
+
"manager",
|
|
865
|
+
"member",
|
|
866
|
+
"officer",
|
|
867
|
+
"owner",
|
|
868
|
+
"partner",
|
|
869
|
+
"president",
|
|
870
|
+
"principal",
|
|
871
|
+
"professor",
|
|
872
|
+
"researcher",
|
|
873
|
+
"scientist"
|
|
874
|
+
]);
|
|
875
|
+
var PROFILE_IDENTITY_EMPLOYMENT_CONTINUATION_STARTS = new Set([
|
|
876
|
+
"appointed",
|
|
877
|
+
"employed",
|
|
878
|
+
"founded",
|
|
879
|
+
"joined",
|
|
880
|
+
"leads",
|
|
881
|
+
"named",
|
|
882
|
+
"owns",
|
|
883
|
+
"serves",
|
|
884
|
+
"serving",
|
|
885
|
+
"works",
|
|
886
|
+
"working"
|
|
887
|
+
]);
|
|
888
|
+
var PROFILE_IDENTITY_LOCATION_CONTINUATION_STARTS = new Set([
|
|
889
|
+
"base",
|
|
890
|
+
"based",
|
|
891
|
+
"lives",
|
|
892
|
+
"located",
|
|
893
|
+
"location",
|
|
894
|
+
"residence",
|
|
895
|
+
"resident",
|
|
896
|
+
"resides"
|
|
897
|
+
]);
|
|
898
|
+
var PROFILE_IDENTITY_SUBJECT_CONTINUATION_DISQUALIFIERS = new Set([
|
|
899
|
+
"another",
|
|
900
|
+
"association",
|
|
901
|
+
"belongs",
|
|
902
|
+
"by",
|
|
903
|
+
"client",
|
|
904
|
+
"colleague",
|
|
905
|
+
"connection",
|
|
906
|
+
"created",
|
|
907
|
+
"discussed",
|
|
908
|
+
"else",
|
|
909
|
+
"follows",
|
|
910
|
+
"impersonator",
|
|
911
|
+
"mentioned",
|
|
912
|
+
"owned",
|
|
913
|
+
"recommends",
|
|
914
|
+
"redirected",
|
|
915
|
+
"redirects",
|
|
916
|
+
"someone",
|
|
917
|
+
"used",
|
|
918
|
+
"uses"
|
|
919
|
+
]);
|
|
920
|
+
var WEBSITE_ATTRIBUTION_NEGATIONS = new Set([
|
|
921
|
+
"criticized",
|
|
922
|
+
"denied",
|
|
923
|
+
"fake",
|
|
924
|
+
"fraudulent",
|
|
925
|
+
"impersonator",
|
|
926
|
+
"not",
|
|
927
|
+
"unofficial",
|
|
928
|
+
"unrelated"
|
|
929
|
+
]);
|
|
930
|
+
var TIER_ONE_ROLE_NOUNS = new Set([
|
|
931
|
+
"accountant",
|
|
932
|
+
"administrator",
|
|
933
|
+
"adviser",
|
|
934
|
+
"advisor",
|
|
935
|
+
"analyst",
|
|
936
|
+
"architect",
|
|
937
|
+
"artist",
|
|
938
|
+
"associate",
|
|
939
|
+
"attorney",
|
|
940
|
+
"author",
|
|
941
|
+
"builder",
|
|
942
|
+
"chair",
|
|
943
|
+
"chairman",
|
|
944
|
+
"chairperson",
|
|
945
|
+
"chief",
|
|
946
|
+
"clinician",
|
|
947
|
+
"coach",
|
|
948
|
+
"commissioner",
|
|
949
|
+
"consultant",
|
|
950
|
+
"controller",
|
|
951
|
+
"coordinator",
|
|
952
|
+
"counsel",
|
|
953
|
+
"creator",
|
|
954
|
+
"cto",
|
|
955
|
+
"ceo",
|
|
956
|
+
"cfo",
|
|
957
|
+
"cio",
|
|
958
|
+
"cmo",
|
|
959
|
+
"coo",
|
|
960
|
+
"dean",
|
|
961
|
+
"designer",
|
|
962
|
+
"developer",
|
|
963
|
+
"director",
|
|
964
|
+
"editor",
|
|
965
|
+
"engineer",
|
|
966
|
+
"entrepreneur",
|
|
967
|
+
"evangelist",
|
|
968
|
+
"executive",
|
|
969
|
+
"fellow",
|
|
970
|
+
"founder",
|
|
971
|
+
"head",
|
|
972
|
+
"investor",
|
|
973
|
+
"lead",
|
|
974
|
+
"leader",
|
|
975
|
+
"lecturer",
|
|
976
|
+
"manager",
|
|
977
|
+
"member",
|
|
978
|
+
"officer",
|
|
979
|
+
"operator",
|
|
980
|
+
"owner",
|
|
981
|
+
"partner",
|
|
982
|
+
"physician",
|
|
983
|
+
"president",
|
|
984
|
+
"principal",
|
|
985
|
+
"producer",
|
|
986
|
+
"professor",
|
|
987
|
+
"recruiter",
|
|
988
|
+
"researcher",
|
|
989
|
+
"scientist",
|
|
990
|
+
"specialist",
|
|
991
|
+
"strategist",
|
|
992
|
+
"supervisor",
|
|
993
|
+
"svp",
|
|
994
|
+
"trustee",
|
|
995
|
+
"vp"
|
|
996
|
+
]);
|
|
997
|
+
var TIER_ONE_ROLE_WORDS = new Set([
|
|
998
|
+
...TIER_ONE_ROLE_NOUNS,
|
|
999
|
+
"acquisition",
|
|
1000
|
+
"ai",
|
|
1001
|
+
"artificial",
|
|
1002
|
+
"assistant",
|
|
1003
|
+
"banking",
|
|
1004
|
+
"board",
|
|
1005
|
+
"business",
|
|
1006
|
+
"clinical",
|
|
1007
|
+
"commercial",
|
|
1008
|
+
"community",
|
|
1009
|
+
"co",
|
|
1010
|
+
"creative",
|
|
1011
|
+
"customer",
|
|
1012
|
+
"data",
|
|
1013
|
+
"design",
|
|
1014
|
+
"development",
|
|
1015
|
+
"digital",
|
|
1016
|
+
"engineering",
|
|
1017
|
+
"executive",
|
|
1018
|
+
"finance",
|
|
1019
|
+
"financial",
|
|
1020
|
+
"founding",
|
|
1021
|
+
"fractional",
|
|
1022
|
+
"general",
|
|
1023
|
+
"global",
|
|
1024
|
+
"growth",
|
|
1025
|
+
"independent",
|
|
1026
|
+
"intelligence",
|
|
1027
|
+
"investment",
|
|
1028
|
+
"i",
|
|
1029
|
+
"ii",
|
|
1030
|
+
"iii",
|
|
1031
|
+
"iv",
|
|
1032
|
+
"junior",
|
|
1033
|
+
"legal",
|
|
1034
|
+
"learning",
|
|
1035
|
+
"machine",
|
|
1036
|
+
"managing",
|
|
1037
|
+
"marketing",
|
|
1038
|
+
"medical",
|
|
1039
|
+
"operations",
|
|
1040
|
+
"people",
|
|
1041
|
+
"product",
|
|
1042
|
+
"program",
|
|
1043
|
+
"project",
|
|
1044
|
+
"regional",
|
|
1045
|
+
"research",
|
|
1046
|
+
"sales",
|
|
1047
|
+
"security",
|
|
1048
|
+
"senior",
|
|
1049
|
+
"site",
|
|
1050
|
+
"software",
|
|
1051
|
+
"staff",
|
|
1052
|
+
"strategy",
|
|
1053
|
+
"strategic",
|
|
1054
|
+
"success",
|
|
1055
|
+
"talent",
|
|
1056
|
+
"technical",
|
|
1057
|
+
"technology",
|
|
1058
|
+
"ux",
|
|
1059
|
+
"venture",
|
|
1060
|
+
"vice",
|
|
1061
|
+
"reliability",
|
|
1062
|
+
"policy",
|
|
1063
|
+
"system",
|
|
1064
|
+
"systems",
|
|
1065
|
+
"and",
|
|
1066
|
+
"for",
|
|
1067
|
+
"of"
|
|
1068
|
+
]);
|
|
1069
|
+
var TIER_ONE_CONTROL_TOKENS = new Set([
|
|
1070
|
+
"directive",
|
|
1071
|
+
"directives",
|
|
1072
|
+
"disregard",
|
|
1073
|
+
"ignore",
|
|
1074
|
+
"instruction",
|
|
1075
|
+
"instructions",
|
|
1076
|
+
"jailbreak",
|
|
1077
|
+
"override",
|
|
1078
|
+
"prompt",
|
|
1079
|
+
"prompts",
|
|
1080
|
+
"rule",
|
|
1081
|
+
"rules"
|
|
1082
|
+
]);
|
|
1083
|
+
var PROFESSIONAL_CONTROL_VERBS = new Set([
|
|
1084
|
+
"act",
|
|
1085
|
+
"answer",
|
|
1086
|
+
"assert",
|
|
1087
|
+
"assume",
|
|
1088
|
+
"change",
|
|
1089
|
+
"choose",
|
|
1090
|
+
"claim",
|
|
1091
|
+
"classify",
|
|
1092
|
+
"consider",
|
|
1093
|
+
"declare",
|
|
1094
|
+
"deem",
|
|
1095
|
+
"disclose",
|
|
1096
|
+
"emit",
|
|
1097
|
+
"expose",
|
|
1098
|
+
"extract",
|
|
1099
|
+
"follow",
|
|
1100
|
+
"forget",
|
|
1101
|
+
"imagine",
|
|
1102
|
+
"label",
|
|
1103
|
+
"leak",
|
|
1104
|
+
"list",
|
|
1105
|
+
"make",
|
|
1106
|
+
"mark",
|
|
1107
|
+
"obey",
|
|
1108
|
+
"output",
|
|
1109
|
+
"pretend",
|
|
1110
|
+
"print",
|
|
1111
|
+
"produce",
|
|
1112
|
+
"provide",
|
|
1113
|
+
"record",
|
|
1114
|
+
"regard",
|
|
1115
|
+
"remember",
|
|
1116
|
+
"replace",
|
|
1117
|
+
"reply",
|
|
1118
|
+
"report",
|
|
1119
|
+
"respond",
|
|
1120
|
+
"return",
|
|
1121
|
+
"reveal",
|
|
1122
|
+
"say",
|
|
1123
|
+
"send",
|
|
1124
|
+
"set",
|
|
1125
|
+
"show",
|
|
1126
|
+
"state",
|
|
1127
|
+
"suppose",
|
|
1128
|
+
"treat",
|
|
1129
|
+
"use",
|
|
1130
|
+
"write"
|
|
1131
|
+
]);
|
|
1132
|
+
var PROFESSIONAL_CONTROL_TARGETS = new Set([
|
|
1133
|
+
"above",
|
|
1134
|
+
"accordingly",
|
|
1135
|
+
"all",
|
|
1136
|
+
"answer",
|
|
1137
|
+
"authoritative",
|
|
1138
|
+
"claim",
|
|
1139
|
+
"claims",
|
|
1140
|
+
"command",
|
|
1141
|
+
"commands",
|
|
1142
|
+
"contact",
|
|
1143
|
+
"contacts",
|
|
1144
|
+
"correct",
|
|
1145
|
+
"data",
|
|
1146
|
+
"email",
|
|
1147
|
+
"emails",
|
|
1148
|
+
"every",
|
|
1149
|
+
"everything",
|
|
1150
|
+
"extractor",
|
|
1151
|
+
"extractors",
|
|
1152
|
+
"following",
|
|
1153
|
+
"follows",
|
|
1154
|
+
"hidden",
|
|
1155
|
+
"message",
|
|
1156
|
+
"messages",
|
|
1157
|
+
"name",
|
|
1158
|
+
"names",
|
|
1159
|
+
"notice",
|
|
1160
|
+
"extracted",
|
|
1161
|
+
"organization",
|
|
1162
|
+
"organizations",
|
|
1163
|
+
"person",
|
|
1164
|
+
"previous",
|
|
1165
|
+
"response",
|
|
1166
|
+
"responses",
|
|
1167
|
+
"role",
|
|
1168
|
+
"roles",
|
|
1169
|
+
"secret",
|
|
1170
|
+
"secrets",
|
|
1171
|
+
"sentence",
|
|
1172
|
+
"statement",
|
|
1173
|
+
"system",
|
|
1174
|
+
"task",
|
|
1175
|
+
"text",
|
|
1176
|
+
"this",
|
|
1177
|
+
"true",
|
|
1178
|
+
"trusted",
|
|
1179
|
+
"value",
|
|
1180
|
+
"values",
|
|
1181
|
+
"below"
|
|
1182
|
+
]);
|
|
1183
|
+
var PROFESSIONAL_CONTROL_CLAUSE_INITIAL_VERBS = new Set([
|
|
1184
|
+
"answer",
|
|
1185
|
+
"assert",
|
|
1186
|
+
"assume",
|
|
1187
|
+
"change",
|
|
1188
|
+
"choose",
|
|
1189
|
+
"claim",
|
|
1190
|
+
"classify",
|
|
1191
|
+
"consider",
|
|
1192
|
+
"declare",
|
|
1193
|
+
"deem",
|
|
1194
|
+
"disregard",
|
|
1195
|
+
"emit",
|
|
1196
|
+
"extract",
|
|
1197
|
+
"forget",
|
|
1198
|
+
"ignore",
|
|
1199
|
+
"imagine",
|
|
1200
|
+
"label",
|
|
1201
|
+
"list",
|
|
1202
|
+
"make",
|
|
1203
|
+
"mark",
|
|
1204
|
+
"obey",
|
|
1205
|
+
"output",
|
|
1206
|
+
"pretend",
|
|
1207
|
+
"print",
|
|
1208
|
+
"produce",
|
|
1209
|
+
"provide",
|
|
1210
|
+
"record",
|
|
1211
|
+
"regard",
|
|
1212
|
+
"remember",
|
|
1213
|
+
"replace",
|
|
1214
|
+
"reply",
|
|
1215
|
+
"report",
|
|
1216
|
+
"respond",
|
|
1217
|
+
"return",
|
|
1218
|
+
"reveal",
|
|
1219
|
+
"say",
|
|
1220
|
+
"set",
|
|
1221
|
+
"state",
|
|
1222
|
+
"suppose",
|
|
1223
|
+
"treat",
|
|
1224
|
+
"use",
|
|
1225
|
+
"write"
|
|
1226
|
+
]);
|
|
1227
|
+
var TIER_ONE_ROLE_CONNECTORS = new Set(["and", "for", "of"]);
|
|
1228
|
+
var GENERIC_PROFESSIONAL_DEPARTURE_TARGETS = new Set([
|
|
1229
|
+
"a",
|
|
1230
|
+
"an",
|
|
1231
|
+
"business",
|
|
1232
|
+
"company",
|
|
1233
|
+
"employer",
|
|
1234
|
+
"employment",
|
|
1235
|
+
"firm",
|
|
1236
|
+
"her",
|
|
1237
|
+
"his",
|
|
1238
|
+
"its",
|
|
1239
|
+
"job",
|
|
1240
|
+
"organization",
|
|
1241
|
+
"position",
|
|
1242
|
+
"role",
|
|
1243
|
+
"startup",
|
|
1244
|
+
"team",
|
|
1245
|
+
"that",
|
|
1246
|
+
"the",
|
|
1247
|
+
"their",
|
|
1248
|
+
"this"
|
|
1249
|
+
]);
|
|
1250
|
+
var IDENTITY_GRADE_CONTENT_FREE_TEXT = new Set([
|
|
1251
|
+
"directory listing",
|
|
1252
|
+
"identity confirmation",
|
|
1253
|
+
"identity verified",
|
|
1254
|
+
"known contact",
|
|
1255
|
+
"known identity",
|
|
1256
|
+
"no additional structured facts",
|
|
1257
|
+
"no structured facts",
|
|
1258
|
+
"profile",
|
|
1259
|
+
"profile page",
|
|
1260
|
+
"professional profile",
|
|
1261
|
+
"public directory listing",
|
|
1262
|
+
"public profile",
|
|
1263
|
+
"public professional profile",
|
|
1264
|
+
"verified",
|
|
1265
|
+
"verified contact",
|
|
1266
|
+
"verified identity",
|
|
1267
|
+
"verified profile",
|
|
1268
|
+
normalizedEvidenceText(exactProfileTitleOnlyEvidenceExcerpt)
|
|
1269
|
+
]);
|
|
1270
|
+
function canonicalJson(value) {
|
|
1271
|
+
if (value === null || typeof value !== "object")
|
|
1272
|
+
return JSON.stringify(value);
|
|
1273
|
+
if (Array.isArray(value))
|
|
1274
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1275
|
+
return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
1276
|
+
}
|
|
1277
|
+
function sha256(value) {
|
|
1278
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/local/cloud-configuration.ts
|
|
1282
|
+
import { z as z3 } from "zod";
|
|
1283
|
+
|
|
1284
|
+
// src/local/config.ts
|
|
1285
|
+
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
1286
|
+
import { z as z2 } from "zod";
|
|
1287
|
+
|
|
1288
|
+
// src/local/paths.ts
|
|
1289
|
+
import { homedir, hostname, platform } from "os";
|
|
1290
|
+
import { join } from "path";
|
|
1291
|
+
function peoplebladeDirectory() {
|
|
1292
|
+
if (platform() === "darwin")
|
|
1293
|
+
return join(homedir(), "Library", "Application Support", "PeopleBlade");
|
|
1294
|
+
if (platform() === "win32")
|
|
1295
|
+
return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "PeopleBlade");
|
|
1296
|
+
return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "peopleblade");
|
|
1297
|
+
}
|
|
1298
|
+
function peoplebladeConfigPath() {
|
|
1299
|
+
return join(peoplebladeDirectory(), "config.json");
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/local/config.ts
|
|
1303
|
+
var configSchema = z2.object({
|
|
1304
|
+
cloud: z2.object({
|
|
1305
|
+
baseUrl: z2.url().max(2048),
|
|
1306
|
+
deviceId: z2.uuid(),
|
|
1307
|
+
token: z2.string().min(20).max(512)
|
|
1308
|
+
}).strict().nullable()
|
|
1309
|
+
}).strict();
|
|
1310
|
+
function readLocalConfig() {
|
|
1311
|
+
const path = peoplebladeConfigPath();
|
|
1312
|
+
if (!existsSync(path))
|
|
1313
|
+
return { cloud: null };
|
|
1314
|
+
return configSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/local/cloud-configuration.ts
|
|
1318
|
+
var cloudConfigurationSchema = z3.object({
|
|
1319
|
+
baseUrl: z3.url().max(2048),
|
|
1320
|
+
deviceId: z3.uuid(),
|
|
1321
|
+
token: z3.string().min(20).max(512)
|
|
1322
|
+
}).strict();
|
|
1323
|
+
function cloudConfiguration(override) {
|
|
1324
|
+
const config = override ?? readLocalConfig().cloud;
|
|
1325
|
+
if (config === null)
|
|
1326
|
+
throw new Error("Run `peopleblade cloud signin` first.");
|
|
1327
|
+
return cloudConfigurationSchema.parse(config);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/local/contact-read-model.ts
|
|
1331
|
+
var canonicalInteractionRollupCtesSql = `
|
|
1332
|
+
canonical_interaction_inputs AS (
|
|
1333
|
+
SELECT member.canonical_person_id AS person_id,
|
|
1334
|
+
metric.provider,
|
|
1335
|
+
CASE WHEN metric.provider = 'beeper'
|
|
1336
|
+
THEN coalesce(metric_realm.service, metric.provider) ELSE metric.provider END AS logical_service,
|
|
1337
|
+
CASE WHEN metric.provider = 'beeper'
|
|
1338
|
+
THEN 'beeper' ELSE 'direct' END AS source_lane,
|
|
1339
|
+
metric.interaction_count,
|
|
1340
|
+
metric.reciprocal,
|
|
1341
|
+
metric.first_interaction_at,
|
|
1342
|
+
metric.last_interaction_at
|
|
1343
|
+
FROM interaction_metrics metric
|
|
1344
|
+
JOIN person_identity_components member ON member.person_id = metric.person_id
|
|
1345
|
+
LEFT JOIN source_realms metric_realm
|
|
1346
|
+
ON metric_realm.authority = metric.provider AND metric_realm.account_key = metric.account_key
|
|
1347
|
+
WHERE (metric_realm.id IS NULL OR metric_realm.active = 1)
|
|
1348
|
+
AND (
|
|
1349
|
+
NOT EXISTS (
|
|
1350
|
+
SELECT 1
|
|
1351
|
+
FROM provider_resources observed_resource
|
|
1352
|
+
WHERE observed_resource.provider = metric.provider
|
|
1353
|
+
AND observed_resource.account_key = metric.account_key
|
|
1354
|
+
AND observed_resource.person_id = metric.person_id
|
|
1355
|
+
)
|
|
1356
|
+
OR EXISTS (
|
|
1357
|
+
SELECT 1
|
|
1358
|
+
FROM provider_resources active_resource
|
|
1359
|
+
LEFT JOIN source_realms active_realm ON active_realm.id = active_resource.source_realm_id
|
|
1360
|
+
WHERE active_resource.provider = metric.provider
|
|
1361
|
+
AND active_resource.account_key = metric.account_key
|
|
1362
|
+
AND active_resource.person_id = metric.person_id
|
|
1363
|
+
AND active_resource.active = 1
|
|
1364
|
+
AND (active_realm.id IS NULL OR active_realm.active = 1)
|
|
1365
|
+
)
|
|
1366
|
+
)
|
|
1367
|
+
),
|
|
1368
|
+
canonical_interaction_lanes AS (
|
|
1369
|
+
SELECT person_id, logical_service, source_lane,
|
|
1370
|
+
sum(interaction_count) AS interaction_count,
|
|
1371
|
+
max(reciprocal) AS reciprocal,
|
|
1372
|
+
min(first_interaction_at) AS first_interaction_at,
|
|
1373
|
+
max(last_interaction_at) AS last_interaction_at
|
|
1374
|
+
FROM canonical_interaction_inputs
|
|
1375
|
+
GROUP BY person_id, logical_service, source_lane
|
|
1376
|
+
),
|
|
1377
|
+
canonical_interaction_services AS (
|
|
1378
|
+
SELECT person_id, logical_service,
|
|
1379
|
+
max(interaction_count) AS interaction_count,
|
|
1380
|
+
max(reciprocal) AS reciprocal,
|
|
1381
|
+
min(first_interaction_at) AS first_interaction_at,
|
|
1382
|
+
max(last_interaction_at) AS last_interaction_at
|
|
1383
|
+
FROM canonical_interaction_lanes
|
|
1384
|
+
GROUP BY person_id, logical_service
|
|
1385
|
+
),
|
|
1386
|
+
canonical_interaction_rollup AS (
|
|
1387
|
+
SELECT person_id,
|
|
1388
|
+
sum(interaction_count) AS interaction_count,
|
|
1389
|
+
max(reciprocal) AS reciprocal,
|
|
1390
|
+
min(first_interaction_at) AS first_interaction_at,
|
|
1391
|
+
max(last_interaction_at) AS last_interaction_at
|
|
1392
|
+
FROM canonical_interaction_services
|
|
1393
|
+
GROUP BY person_id
|
|
1394
|
+
)
|
|
1395
|
+
`;
|
|
1396
|
+
|
|
1397
|
+
// src/local/provider-handles.ts
|
|
1398
|
+
import { createHmac } from "crypto";
|
|
1399
|
+
import { z as z4 } from "zod";
|
|
1400
|
+
var sha256Schema = z4.string().regex(/^[a-f0-9]{64}$/u);
|
|
1401
|
+
var providerHandleCandidateSchema = z4.object({
|
|
1402
|
+
provider: z4.string().min(1).max(64),
|
|
1403
|
+
service: z4.string().min(1).max(64).nullable(),
|
|
1404
|
+
realmExternalIdSha256: sha256Schema.nullable(),
|
|
1405
|
+
bindingAuthSha256: sha256Schema.nullable(),
|
|
1406
|
+
handle: z4.string().min(1)
|
|
1407
|
+
}).strict();
|
|
1408
|
+
function localDatabaseInstanceId(database) {
|
|
1409
|
+
const row = database.query("SELECT instance_id FROM local_state WHERE singleton = 1").get();
|
|
1410
|
+
if (row === null || !/^[a-f0-9]{64}$/u.test(row.instance_id)) {
|
|
1411
|
+
throw new Error("PeopleBlade local database identity is missing.");
|
|
1412
|
+
}
|
|
1413
|
+
return row.instance_id;
|
|
1414
|
+
}
|
|
1415
|
+
function providerHandleKey(candidate, databaseInstanceId) {
|
|
1416
|
+
if (candidate.provider !== "beeper")
|
|
1417
|
+
return candidate.provider;
|
|
1418
|
+
if (candidate.service === null || candidate.realmExternalIdSha256 === null || candidate.bindingAuthSha256 === null)
|
|
1419
|
+
throw new Error("Beeper provider handle is missing its bound source realm.");
|
|
1420
|
+
const realmDiscriminator = createHmac("sha256", databaseInstanceId).update(canonicalJson([
|
|
1421
|
+
"peopleblade-beeper-provider-handle-realm-v1",
|
|
1422
|
+
candidate.bindingAuthSha256,
|
|
1423
|
+
candidate.realmExternalIdSha256
|
|
1424
|
+
])).digest("hex").slice(0, 48);
|
|
1425
|
+
return `beeper:${candidate.service}:${realmDiscriminator}`;
|
|
1426
|
+
}
|
|
1427
|
+
function parseProviderHandleCandidates(value, databaseInstanceId) {
|
|
1428
|
+
const localSalt = sha256Schema.parse(databaseInstanceId);
|
|
1429
|
+
const parsed = JSON.parse(value);
|
|
1430
|
+
const candidates = z4.array(providerHandleCandidateSchema).max(50).parse(parsed);
|
|
1431
|
+
const handles = {};
|
|
1432
|
+
for (const candidate of candidates) {
|
|
1433
|
+
const key = providerHandleKey(candidate, localSalt);
|
|
1434
|
+
if (Object.hasOwn(handles, key)) {
|
|
1435
|
+
throw new Error("Local provider handles contain a stable-key collision.");
|
|
1436
|
+
}
|
|
1437
|
+
handles[key] = candidate.handle;
|
|
1438
|
+
}
|
|
1439
|
+
return handles;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/local/projection.ts
|
|
1443
|
+
function parseStringArray(value) {
|
|
1444
|
+
const parsed = JSON.parse(value);
|
|
1445
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string"))
|
|
1446
|
+
throw new Error("Invalid local contact array.");
|
|
1447
|
+
return parsed;
|
|
1448
|
+
}
|
|
1449
|
+
function iso(value) {
|
|
1450
|
+
if (value === null)
|
|
1451
|
+
return null;
|
|
1452
|
+
const parsed = new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`);
|
|
1453
|
+
return Number.isNaN(parsed.valueOf()) ? null : parsed.toISOString();
|
|
1454
|
+
}
|
|
1455
|
+
function localDatabaseFingerprint(database) {
|
|
1456
|
+
return localDatabaseInstanceId(database);
|
|
1457
|
+
}
|
|
1458
|
+
function localSchemaVersion(database) {
|
|
1459
|
+
const row = database.query("SELECT count(*) AS count FROM schema_migrations").get();
|
|
1460
|
+
if (row === null || !Number.isSafeInteger(row.count) || row.count < 1)
|
|
1461
|
+
throw new Error("PeopleBlade schema is not initialized.");
|
|
1462
|
+
return row.count;
|
|
1463
|
+
}
|
|
1464
|
+
function projectCloudContacts(database, options = {}) {
|
|
1465
|
+
const selected = options.personIds === undefined ? undefined : [...new Set(options.personIds)];
|
|
1466
|
+
if (selected !== undefined && (selected.length > 100 || selected.some((id) => !Number.isSafeInteger(id) || id < 1))) {
|
|
1467
|
+
throw new Error("Projection selection requires at most 100 positive canonical person IDs.");
|
|
1468
|
+
}
|
|
1469
|
+
if (selected?.length === 0)
|
|
1470
|
+
return [];
|
|
1471
|
+
const selectionSql = selected === undefined ? "" : `AND p.id IN (${selected.map(() => "?").join(",")})`;
|
|
1472
|
+
const rows = database.query(`
|
|
1473
|
+
WITH
|
|
1474
|
+
${canonicalInteractionRollupCtesSql},
|
|
1475
|
+
person_rollup AS (
|
|
1476
|
+
SELECT member.canonical_person_id AS person_id,
|
|
1477
|
+
CASE WHEN count(DISTINCT person.birthday) = 1 THEN min(person.birthday) ELSE NULL END AS birthday,
|
|
1478
|
+
max(person.do_not_contact) AS do_not_contact,
|
|
1479
|
+
max(person.updated_at) AS updated_at
|
|
1480
|
+
FROM person_identity_components member
|
|
1481
|
+
JOIN people person ON person.id = member.person_id
|
|
1482
|
+
GROUP BY member.canonical_person_id
|
|
1483
|
+
),
|
|
1484
|
+
deduplicated_methods AS (
|
|
1485
|
+
SELECT member.canonical_person_id AS person_id, method.kind, method.normalized_value,
|
|
1486
|
+
max(method.is_primary) AS primary_rank, min(method.id) AS first_id,
|
|
1487
|
+
min(CASE WHEN method.identity_eligible = 1 AND method.confidence = 'exact'
|
|
1488
|
+
AND (method.provider_resource_id IS NULL OR (
|
|
1489
|
+
resource.active = 1 AND (realm.id IS NULL OR realm.active = 1)
|
|
1490
|
+
))
|
|
1491
|
+
THEN method.id END) AS identity_anchor_id
|
|
1492
|
+
FROM contact_methods method
|
|
1493
|
+
JOIN person_identity_components member ON member.person_id = method.person_id
|
|
1494
|
+
LEFT JOIN provider_resources resource ON resource.id = method.provider_resource_id
|
|
1495
|
+
LEFT JOIN source_realms realm ON realm.id = resource.source_realm_id
|
|
1496
|
+
WHERE method.active = 1 AND method.kind IN ('email', 'phone')
|
|
1497
|
+
GROUP BY member.canonical_person_id, method.kind, method.normalized_value
|
|
1498
|
+
),
|
|
1499
|
+
ordered_methods AS (
|
|
1500
|
+
SELECT item.person_id, item.kind, method.value,
|
|
1501
|
+
item.identity_anchor_id IS NOT NULL AS identity_anchor
|
|
1502
|
+
FROM deduplicated_methods item
|
|
1503
|
+
JOIN contact_methods method ON method.id = coalesce(item.identity_anchor_id, item.first_id)
|
|
1504
|
+
ORDER BY item.person_id, item.kind, item.identity_anchor_id IS NOT NULL DESC,
|
|
1505
|
+
item.primary_rank DESC, item.first_id
|
|
1506
|
+
),
|
|
1507
|
+
email_rollup AS (
|
|
1508
|
+
SELECT person_id, json_group_array(value) AS emails_json
|
|
1509
|
+
FROM ordered_methods WHERE kind = 'email' GROUP BY person_id
|
|
1510
|
+
),
|
|
1511
|
+
phone_rollup AS (
|
|
1512
|
+
SELECT person_id, json_group_array(value) AS phones_json
|
|
1513
|
+
FROM ordered_methods WHERE kind = 'phone' GROUP BY person_id
|
|
1514
|
+
),
|
|
1515
|
+
identity_email_rollup AS (
|
|
1516
|
+
SELECT person_id, json_group_array(value) AS identity_emails_json
|
|
1517
|
+
FROM ordered_methods WHERE kind = 'email' AND identity_anchor GROUP BY person_id
|
|
1518
|
+
),
|
|
1519
|
+
identity_phone_rollup AS (
|
|
1520
|
+
SELECT person_id, json_group_array(value) AS identity_phones_json
|
|
1521
|
+
FROM ordered_methods WHERE kind = 'phone' AND identity_anchor GROUP BY person_id
|
|
1522
|
+
),
|
|
1523
|
+
identity_profile_url_rollup AS (
|
|
1524
|
+
SELECT person_id, json_group_array(profile_url) AS identity_profile_urls_json
|
|
1525
|
+
FROM (
|
|
1526
|
+
SELECT member.canonical_person_id AS person_id,
|
|
1527
|
+
trim(resource.profile_url) AS profile_url, min(resource.id) AS first_id
|
|
1528
|
+
FROM provider_resources resource
|
|
1529
|
+
JOIN person_identity_components member ON member.person_id = resource.person_id
|
|
1530
|
+
LEFT JOIN source_realms realm ON realm.id = resource.source_realm_id
|
|
1531
|
+
WHERE resource.active = 1 AND resource.profile_url_identity_eligible = 1
|
|
1532
|
+
AND resource.profile_url IS NOT NULL AND trim(resource.profile_url) <> ''
|
|
1533
|
+
AND (realm.id IS NULL OR realm.active = 1)
|
|
1534
|
+
GROUP BY member.canonical_person_id, trim(resource.profile_url)
|
|
1535
|
+
ORDER BY member.canonical_person_id, first_id
|
|
1536
|
+
)
|
|
1537
|
+
GROUP BY person_id
|
|
1538
|
+
),
|
|
1539
|
+
resource_eligibility AS (
|
|
1540
|
+
SELECT member.canonical_person_id AS person_id,
|
|
1541
|
+
max(resource.active) AS has_active_resource
|
|
1542
|
+
FROM provider_resources resource
|
|
1543
|
+
JOIN person_identity_components member ON member.person_id = resource.person_id
|
|
1544
|
+
GROUP BY member.canonical_person_id
|
|
1545
|
+
),
|
|
1546
|
+
source_values AS (
|
|
1547
|
+
SELECT member.canonical_person_id AS person_id, resource.provider
|
|
1548
|
+
FROM provider_resources resource
|
|
1549
|
+
JOIN person_identity_components member ON member.person_id = resource.person_id
|
|
1550
|
+
WHERE resource.active = 1
|
|
1551
|
+
UNION
|
|
1552
|
+
SELECT metric.person_id, metric.provider
|
|
1553
|
+
FROM canonical_interaction_inputs metric
|
|
1554
|
+
),
|
|
1555
|
+
source_rollup AS (
|
|
1556
|
+
SELECT person_id, json_group_array(provider) AS sources_json
|
|
1557
|
+
FROM (SELECT person_id, provider FROM source_values ORDER BY person_id, provider)
|
|
1558
|
+
GROUP BY person_id
|
|
1559
|
+
),
|
|
1560
|
+
handle_choices AS (
|
|
1561
|
+
SELECT person_id, provider, service, realm_external_id_sha256,
|
|
1562
|
+
binding_auth_sha256, resource_id
|
|
1563
|
+
FROM (
|
|
1564
|
+
SELECT member.canonical_person_id AS person_id,
|
|
1565
|
+
resource.provider,
|
|
1566
|
+
CASE WHEN resource.provider='beeper' THEN realm.service ELSE NULL END AS service,
|
|
1567
|
+
CASE WHEN resource.provider='beeper' THEN realm.external_id_sha256 ELSE NULL END AS realm_external_id_sha256,
|
|
1568
|
+
CASE WHEN resource.provider='beeper' THEN incarnation.auth_sha256 ELSE NULL END AS binding_auth_sha256,
|
|
1569
|
+
resource.id AS resource_id,
|
|
1570
|
+
row_number() OVER (
|
|
1571
|
+
PARTITION BY member.canonical_person_id, resource.provider,
|
|
1572
|
+
CASE WHEN resource.provider='beeper' THEN resource.source_realm_id ELSE NULL END
|
|
1573
|
+
ORDER BY CASE
|
|
1574
|
+
WHEN resource.profile_url IS NOT NULL AND trim(resource.profile_url) <> '' THEN 0
|
|
1575
|
+
WHEN resource.username IS NOT NULL AND trim(resource.username) <> '' THEN 1
|
|
1576
|
+
ELSE 2
|
|
1577
|
+
END, resource.id
|
|
1578
|
+
) AS preference_rank
|
|
1579
|
+
FROM provider_resources resource
|
|
1580
|
+
JOIN person_identity_components member ON member.person_id = resource.person_id
|
|
1581
|
+
LEFT JOIN source_realms realm ON realm.id = resource.source_realm_id
|
|
1582
|
+
LEFT JOIN source_bindings binding ON binding.id = realm.source_binding_id
|
|
1583
|
+
LEFT JOIN current_source_binding_incarnations incarnation ON incarnation.source_binding_id = binding.id
|
|
1584
|
+
WHERE resource.active = 1 AND (realm.id IS NULL OR realm.active = 1)
|
|
1585
|
+
)
|
|
1586
|
+
WHERE preference_rank = 1
|
|
1587
|
+
),
|
|
1588
|
+
handle_rollup AS (
|
|
1589
|
+
SELECT person_id, json_group_array(json_object(
|
|
1590
|
+
'provider', provider,
|
|
1591
|
+
'service', service,
|
|
1592
|
+
'realmExternalIdSha256', realm_external_id_sha256,
|
|
1593
|
+
'bindingAuthSha256', binding_auth_sha256,
|
|
1594
|
+
'handle', handle
|
|
1595
|
+
)) AS handles_json
|
|
1596
|
+
FROM (
|
|
1597
|
+
SELECT choice.person_id, choice.provider, choice.service,
|
|
1598
|
+
choice.realm_external_id_sha256, choice.binding_auth_sha256,
|
|
1599
|
+
CASE
|
|
1600
|
+
WHEN resource.profile_url IS NOT NULL AND trim(resource.profile_url) <> '' THEN resource.profile_url
|
|
1601
|
+
WHEN resource.username IS NOT NULL AND trim(resource.username) <> '' THEN resource.username
|
|
1602
|
+
ELSE resource.resource_id
|
|
1603
|
+
END AS handle
|
|
1604
|
+
FROM handle_choices choice
|
|
1605
|
+
JOIN provider_resources resource ON resource.id = choice.resource_id
|
|
1606
|
+
ORDER BY choice.person_id, choice.provider, choice.service,
|
|
1607
|
+
choice.realm_external_id_sha256
|
|
1608
|
+
)
|
|
1609
|
+
GROUP BY person_id
|
|
1610
|
+
)
|
|
1611
|
+
SELECT p.id, p.display_name, p.organization, p.title,
|
|
1612
|
+
person_rollup.birthday,
|
|
1613
|
+
coalesce(person_rollup.do_not_contact, p.do_not_contact) AS do_not_contact,
|
|
1614
|
+
coalesce(person_rollup.updated_at, p.updated_at) AS updated_at,
|
|
1615
|
+
coalesce(canonical_interaction_rollup.interaction_count, 0) AS interaction_count,
|
|
1616
|
+
coalesce(canonical_interaction_rollup.reciprocal, 0) AS reciprocal,
|
|
1617
|
+
canonical_interaction_rollup.first_interaction_at,
|
|
1618
|
+
canonical_interaction_rollup.last_interaction_at,
|
|
1619
|
+
coalesce(email_rollup.emails_json, '[]') AS emails_json,
|
|
1620
|
+
coalesce(phone_rollup.phones_json, '[]') AS phones_json,
|
|
1621
|
+
coalesce(identity_email_rollup.identity_emails_json, '[]') AS identity_emails_json,
|
|
1622
|
+
coalesce(identity_phone_rollup.identity_phones_json, '[]') AS identity_phones_json,
|
|
1623
|
+
coalesce(identity_profile_url_rollup.identity_profile_urls_json, '[]') AS identity_profile_urls_json,
|
|
1624
|
+
coalesce(source_rollup.sources_json, '[]') AS sources_json,
|
|
1625
|
+
coalesce(handle_rollup.handles_json, '[]') AS handles_json
|
|
1626
|
+
FROM people p
|
|
1627
|
+
JOIN person_identity_components root ON root.person_id = p.id AND root.canonical_person_id = p.id
|
|
1628
|
+
LEFT JOIN person_rollup ON person_rollup.person_id = p.id
|
|
1629
|
+
LEFT JOIN canonical_interaction_rollup ON canonical_interaction_rollup.person_id = p.id
|
|
1630
|
+
LEFT JOIN email_rollup ON email_rollup.person_id = p.id
|
|
1631
|
+
LEFT JOIN phone_rollup ON phone_rollup.person_id = p.id
|
|
1632
|
+
LEFT JOIN identity_email_rollup ON identity_email_rollup.person_id = p.id
|
|
1633
|
+
LEFT JOIN identity_phone_rollup ON identity_phone_rollup.person_id = p.id
|
|
1634
|
+
LEFT JOIN identity_profile_url_rollup ON identity_profile_url_rollup.person_id = p.id
|
|
1635
|
+
LEFT JOIN source_rollup ON source_rollup.person_id = p.id
|
|
1636
|
+
LEFT JOIN handle_rollup ON handle_rollup.person_id = p.id
|
|
1637
|
+
LEFT JOIN resource_eligibility ON resource_eligibility.person_id = p.id
|
|
1638
|
+
WHERE (resource_eligibility.person_id IS NULL
|
|
1639
|
+
OR resource_eligibility.has_active_resource = 1
|
|
1640
|
+
OR canonical_interaction_rollup.person_id IS NOT NULL)
|
|
1641
|
+
${selectionSql}
|
|
1642
|
+
ORDER BY p.id
|
|
1643
|
+
`).all(...selected ?? []);
|
|
1644
|
+
const instance = localDatabaseFingerprint(database);
|
|
1645
|
+
return rows.map((row) => {
|
|
1646
|
+
const sources = sourceLabelSchema.array().parse(parseStringArray(row.sources_json).filter((source) => sourceLabelSchema.safeParse(source).success));
|
|
1647
|
+
const data = {
|
|
1648
|
+
id: sha256(`peopleblade\x00${instance}\x00${row.id}`),
|
|
1649
|
+
localPersonId: String(row.id),
|
|
1650
|
+
displayName: row.display_name ?? "Unnamed contact",
|
|
1651
|
+
emails: parseStringArray(row.emails_json).filter((email) => cloudEmailSchema.safeParse(email).success).slice(0, 100),
|
|
1652
|
+
phones: parseStringArray(row.phones_json).slice(0, 100),
|
|
1653
|
+
organization: row.organization,
|
|
1654
|
+
title: row.title,
|
|
1655
|
+
birthday: row.birthday,
|
|
1656
|
+
sources,
|
|
1657
|
+
providerHandles: parseProviderHandleCandidates(row.handles_json, instance),
|
|
1658
|
+
enrichmentInputVersion: 2,
|
|
1659
|
+
identityAnchors: {
|
|
1660
|
+
state: "available",
|
|
1661
|
+
emails: parseStringArray(row.identity_emails_json).filter((email) => cloudEmailSchema.safeParse(email).success).slice(0, 100),
|
|
1662
|
+
phones: parseStringArray(row.identity_phones_json).slice(0, 100),
|
|
1663
|
+
profileUrls: parseStringArray(row.identity_profile_urls_json).filter((url) => httpUrlSchema.safeParse(url).success).slice(0, 100)
|
|
1664
|
+
},
|
|
1665
|
+
automaticEnrichmentSelection: {
|
|
1666
|
+
version: 1,
|
|
1667
|
+
state: row.do_not_contact === 1 ? "blocked" : "eligible"
|
|
1668
|
+
},
|
|
1669
|
+
interactionCount: row.interaction_count,
|
|
1670
|
+
reciprocal: row.reciprocal === 1,
|
|
1671
|
+
firstInteractionAt: iso(row.first_interaction_at),
|
|
1672
|
+
lastInteractionAt: iso(row.last_interaction_at),
|
|
1673
|
+
updatedAt: iso(row.updated_at) ?? new Date(0).toISOString()
|
|
1674
|
+
};
|
|
1675
|
+
return cloudContactSchema.parse({
|
|
1676
|
+
...data,
|
|
1677
|
+
metadataSha256: sha256(canonicalJson(data)),
|
|
1678
|
+
enrichmentInputSha256: enrichmentInputSha256(data)
|
|
1679
|
+
});
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/local/cloud-sync-runtime.ts
|
|
1684
|
+
import {
|
|
1685
|
+
Cause,
|
|
1686
|
+
Exit,
|
|
1687
|
+
Layer,
|
|
1688
|
+
ManagedRuntime,
|
|
1689
|
+
Option
|
|
1690
|
+
} from "effect";
|
|
1691
|
+
|
|
1692
|
+
// src/local/cloud-sync-program.ts
|
|
1693
|
+
import { Effect as Effect2 } from "effect";
|
|
1694
|
+
|
|
1695
|
+
// src/local/cloud-sync-service.ts
|
|
1696
|
+
import { Context, Data, Effect } from "effect";
|
|
1697
|
+
import { z as z5 } from "zod";
|
|
1698
|
+
|
|
1699
|
+
class CloudSyncError extends Data.TaggedError("CloudSyncError") {
|
|
1700
|
+
}
|
|
1701
|
+
var syncValue = (operation, read) => Effect.try({ try: read, catch: (cause) => new CloudSyncError({ operation, cause }) });
|
|
1702
|
+
var syncStartResponse = z5.object({
|
|
1703
|
+
snapshotId: z5.uuid(),
|
|
1704
|
+
resumed: z5.boolean(),
|
|
1705
|
+
uploadedContacts: z5.number().int().min(0).max(1e6),
|
|
1706
|
+
uploadedPages: z5.number().int().min(0).max(1e5),
|
|
1707
|
+
uploadedPageContactCounts: z5.array(z5.number().int().min(1).max(200)).max(1e5)
|
|
1708
|
+
}).strict().superRefine((value, context) => {
|
|
1709
|
+
if (value.uploadedPageContactCounts.length !== value.uploadedPages)
|
|
1710
|
+
context.addIssue({
|
|
1711
|
+
code: "custom",
|
|
1712
|
+
path: ["uploadedPageContactCounts"],
|
|
1713
|
+
message: "Uploaded sync-page manifest length does not match its page count"
|
|
1714
|
+
});
|
|
1715
|
+
const contactCount = value.uploadedPageContactCounts.reduce((sum, count) => sum + count, 0);
|
|
1716
|
+
if (contactCount !== value.uploadedContacts)
|
|
1717
|
+
context.addIssue({
|
|
1718
|
+
code: "custom",
|
|
1719
|
+
path: ["uploadedContacts"],
|
|
1720
|
+
message: "Uploaded sync-page manifest does not match its contact count"
|
|
1721
|
+
});
|
|
1722
|
+
if (!value.resumed && (value.uploadedContacts !== 0 || value.uploadedPages !== 0))
|
|
1723
|
+
context.addIssue({
|
|
1724
|
+
code: "custom",
|
|
1725
|
+
path: ["resumed"],
|
|
1726
|
+
message: "A fresh sync snapshot cannot report uploaded progress"
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
class CloudSync extends Context.Tag("peopleblade/local/CloudSync/v1")() {
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/local/cloud-sync-program.ts
|
|
1734
|
+
var syncCloudProgram = Effect2.gen(function* () {
|
|
1735
|
+
const ports = yield* CloudSync;
|
|
1736
|
+
const prepared = yield* ports.prepare;
|
|
1737
|
+
const { contacts, pageSize } = prepared;
|
|
1738
|
+
const started = yield* ports.start(prepared.start);
|
|
1739
|
+
if (started.uploadedContacts > contacts.length)
|
|
1740
|
+
return yield* Effect2.fail(new CloudSyncError({
|
|
1741
|
+
operation: "resume",
|
|
1742
|
+
cause: new Error("Cloud sync resume progress exceeds the current local projection.")
|
|
1743
|
+
}));
|
|
1744
|
+
let pages = 0;
|
|
1745
|
+
let offset = 0;
|
|
1746
|
+
for (const priorPageContacts of started.uploadedPageContactCounts) {
|
|
1747
|
+
const replayContacts = contacts.slice(offset, offset + priorPageContacts);
|
|
1748
|
+
if (replayContacts.length !== priorPageContacts)
|
|
1749
|
+
return yield* Effect2.fail(new CloudSyncError({
|
|
1750
|
+
operation: "resume",
|
|
1751
|
+
cause: new Error("Cloud sync resume manifest exceeds the current local projection.")
|
|
1752
|
+
}));
|
|
1753
|
+
const input = yield* syncValue("page", () => syncPageSchema.parse({ snapshotId: started.snapshotId, ordinal: pages, contacts: replayContacts }));
|
|
1754
|
+
yield* ports.page(input);
|
|
1755
|
+
pages += 1;
|
|
1756
|
+
offset += priorPageContacts;
|
|
1757
|
+
}
|
|
1758
|
+
if (offset !== started.uploadedContacts || pages !== started.uploadedPages)
|
|
1759
|
+
return yield* Effect2.fail(new CloudSyncError({
|
|
1760
|
+
operation: "resume",
|
|
1761
|
+
cause: new Error("Cloud sync resume manifest did not reproduce its reported progress.")
|
|
1762
|
+
}));
|
|
1763
|
+
for (;offset < contacts.length; offset += pageSize) {
|
|
1764
|
+
const input = yield* syncValue("page", () => syncPageSchema.parse({ snapshotId: started.snapshotId, ordinal: pages, contacts: contacts.slice(offset, offset + pageSize) }));
|
|
1765
|
+
yield* ports.page(input);
|
|
1766
|
+
pages += 1;
|
|
1767
|
+
}
|
|
1768
|
+
const finish = yield* syncValue("finish", () => syncFinishSchema.parse({ snapshotId: started.snapshotId, pages, contactCount: contacts.length }));
|
|
1769
|
+
yield* ports.finish(finish);
|
|
1770
|
+
yield* ports.commit(started.snapshotId);
|
|
1771
|
+
return { snapshotId: started.snapshotId, contacts: contacts.length, pages, resumed: started.resumed, replayedPages: started.uploadedPages };
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
// src/local/cloud-sync-runtime.ts
|
|
1775
|
+
async function runCloudSync(ports, signal) {
|
|
1776
|
+
const runtime = ManagedRuntime.make(Layer.succeed(CloudSync, ports));
|
|
1777
|
+
try {
|
|
1778
|
+
const exit = await runtime.runPromiseExit(syncCloudProgram, { signal });
|
|
1779
|
+
if (Exit.isSuccess(exit))
|
|
1780
|
+
return exit.value;
|
|
1781
|
+
const failure = Cause.failureOption(exit.cause);
|
|
1782
|
+
if (Option.isSome(failure) && failure.value instanceof CloudSyncError)
|
|
1783
|
+
throw failure.value.cause;
|
|
1784
|
+
throw Cause.squash(exit.cause);
|
|
1785
|
+
} finally {
|
|
1786
|
+
await runtime.dispose();
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/local/cloud-sync-client.ts
|
|
1791
|
+
var SYNC_PAGE_TIMEOUT_MS = 240000;
|
|
1792
|
+
function cloudSyncPorts(database, options, postJson) {
|
|
1793
|
+
let config;
|
|
1794
|
+
const request = (operation, path, input, timeoutMs) => Effect3.suspend(() => {
|
|
1795
|
+
const current = config;
|
|
1796
|
+
if (current === undefined)
|
|
1797
|
+
return Effect3.die(new Error("Cloud sync transport used before preparation."));
|
|
1798
|
+
return Effect3.tryPromise({
|
|
1799
|
+
try: (signal) => postJson(options.fetcher ?? fetch, `${current.baseUrl}${path}`, input, current.token, timeoutMs, signal),
|
|
1800
|
+
catch: (cause) => new CloudSyncError({ operation, cause })
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
return {
|
|
1804
|
+
prepare: syncValue("projection", () => {
|
|
1805
|
+
config = cloudConfiguration(options.configuration);
|
|
1806
|
+
const contacts = projectCloudContacts(database);
|
|
1807
|
+
return {
|
|
1808
|
+
contacts,
|
|
1809
|
+
pageSize: Math.max(1, Math.min(200, options.pageSize ?? 200)),
|
|
1810
|
+
start: syncStartSchema.parse({
|
|
1811
|
+
contactCount: contacts.length,
|
|
1812
|
+
databaseFingerprint: localDatabaseFingerprint(database),
|
|
1813
|
+
schemaVersion: localSchemaVersion(database),
|
|
1814
|
+
resumeVersion: 1
|
|
1815
|
+
})
|
|
1816
|
+
};
|
|
1817
|
+
}),
|
|
1818
|
+
start: (input) => request("start", "/api/cli/sync/start", input, 285000).pipe(Effect3.flatMap((value) => syncValue("start", () => syncStartResponse.parse(value)))),
|
|
1819
|
+
page: (input) => request("page", "/api/cli/sync/page", input, SYNC_PAGE_TIMEOUT_MS).pipe(Effect3.asVoid),
|
|
1820
|
+
finish: (input) => request("finish", "/api/cli/sync/finish", input).pipe(Effect3.asVoid),
|
|
1821
|
+
commit: (snapshotId) => syncValue("local-commit", () => {
|
|
1822
|
+
if (config === undefined)
|
|
1823
|
+
throw new Error("Cloud sync commit used before preparation.");
|
|
1824
|
+
database.query(`INSERT INTO cloud_sync_state(singleton,base_url,device_id,last_snapshot_id,last_synced_at)
|
|
1825
|
+
VALUES (1,?,?,?,CURRENT_TIMESTAMP)
|
|
1826
|
+
ON CONFLICT(singleton) DO UPDATE SET base_url=excluded.base_url, device_id=excluded.device_id,
|
|
1827
|
+
last_snapshot_id=excluded.last_snapshot_id, last_synced_at=excluded.last_synced_at`).run(config.baseUrl, config.deviceId, snapshotId);
|
|
1828
|
+
})
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function startCloudSync(database, options, transport) {
|
|
1832
|
+
return runCloudSync(cloudSyncPorts(database, options, transport), options.signal);
|
|
1833
|
+
}
|
|
1834
|
+
export {
|
|
1835
|
+
startCloudSync
|
|
1836
|
+
};
|