@hraness/ghostget 0.17.5 → 0.18.0
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/CHANGELOG.md +25 -0
- package/README.md +16 -9
- package/dist/apple-photos-client.js +1 -1
- package/dist/beeper-client.js +1 -1
- package/dist/{index-pf74yjs2.js → index-9wca02er.js} +1 -1
- package/docs/control-panel.md +147 -0
- package/package.json +47 -7
- package/skills/ghostget/SKILL.md +3 -1
- package/skills/ghostget/references/control-panel.md +43 -0
- package/skills/ghostget/references/install.md +5 -5
- package/skills/ghostget/references/linkedin-adapter.md +17 -3
- package/skills/ghostget/references/platform-patterns.md +1 -1
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
- package/src/auth.ts +35 -1
- package/src/beeper-client-types.ts +1 -1
- package/src/cli.ts +18 -0
- package/src/confirmed-write-platform.ts +16 -1
- package/src/control/account-revision.ts +16 -0
- package/src/control/activity.ts +104 -0
- package/src/control/approval-broker.ts +59 -0
- package/src/control/approval-client.ts +49 -0
- package/src/control/bundled-interfaces.ts +20 -0
- package/src/control/cli.ts +20 -0
- package/src/control/connections.ts +87 -0
- package/src/control/credential-helper.ts +152 -0
- package/src/control/helper.ts +79 -0
- package/src/control/interface-cli.ts +22 -0
- package/src/control/interface-json.ts +94 -0
- package/src/control/interface-schema.ts +120 -0
- package/src/control/interfaces.ts +438 -0
- package/src/control/protocol.ts +182 -0
- package/src/control/service.ts +104 -0
- package/src/control/validation.ts +103 -0
- package/src/control/vault.ts +105 -0
- package/src/control/web-gateway.ts +62 -0
- package/src/control/web-policy.ts +56 -0
- package/src/ghostget.ts +2 -0
- package/src/messaging-runtime.ts +3 -0
- package/src/oauth-google.ts +11 -5
- package/src/omni-runtime.ts +18 -3
- package/src/operation-permission-store.ts +92 -0
- package/src/operation-permission.ts +308 -0
- package/src/pinned-https.ts +5 -0
- package/src/provider-http.ts +11 -3
- package/src/provider-plugin-contract-identity.ts +2 -2
- package/src/provider-plugin-import-analysis.ts +52 -0
- package/src/provider-plugin-module-analysis.ts +21 -1
- package/src/provider-plugin-registry.ts +4 -8
- package/src/provider-plugin.ts +4 -8
- package/src/providers/linkedin-web-contact.ts +237 -21
- package/src/read-client.ts +12 -2
- package/src/runtime.ts +81 -9
- package/src/state-helper.ts +2 -0
- package/src/storage.ts +71 -1
- package/src/usage.ts +6 -0
- package/src/version.ts +1 -1
package/src/provider-plugin.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
sep,
|
|
26
26
|
} from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { scanProviderPluginValueImports } from "./provider-plugin-import-analysis";
|
|
28
29
|
|
|
29
30
|
import type { GhostgetAuth } from "./auth";
|
|
30
31
|
import type { MessageLikeMeSourceConversationCoordinateBindingV1 } from "./message-like-me-agentic-messaging";
|
|
@@ -1339,10 +1340,6 @@ const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DEPTH = 32;
|
|
|
1339
1340
|
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_PATH_BYTES = 1_024;
|
|
1340
1341
|
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_BYTES = 128 * 1024 * 1024;
|
|
1341
1342
|
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_CACHE_ENTRIES = 256;
|
|
1342
|
-
const providerPluginEvaluationImportScanners = Object.freeze({
|
|
1343
|
-
js: new Bun.Transpiler({ loader: "js" }),
|
|
1344
|
-
ts: new Bun.Transpiler({ loader: "ts" }),
|
|
1345
|
-
});
|
|
1346
1343
|
const providerPluginEvaluationModuleExtensions = new Set([
|
|
1347
1344
|
".cjs",
|
|
1348
1345
|
".cts",
|
|
@@ -2194,11 +2191,10 @@ function providerPluginEvaluationValueImports(
|
|
|
2194
2191
|
`provider plugin evaluation module ${path} must be valid UTF-8`,
|
|
2195
2192
|
);
|
|
2196
2193
|
}
|
|
2197
|
-
const
|
|
2194
|
+
const loader =
|
|
2198
2195
|
extension === ".ts" || extension === ".mts" || extension === ".cts"
|
|
2199
|
-
?
|
|
2200
|
-
|
|
2201
|
-
const imports = Object.freeze([...scanner.scanImports(source)]);
|
|
2196
|
+
? "ts" : "js";
|
|
2197
|
+
const imports = scanProviderPluginValueImports(source, loader);
|
|
2202
2198
|
if (imports.length > MAX_PROVIDER_PLUGIN_EVALUATION_IMPORTS_PER_MODULE) {
|
|
2203
2199
|
throw new Error(
|
|
2204
2200
|
`provider plugin evaluation module ${path} has too many static imports`,
|
|
@@ -42,7 +42,28 @@ const MAX_CODE_TAGS = 256;
|
|
|
42
42
|
const MAX_COMO_ASSIGNMENTS = 8;
|
|
43
43
|
const MAX_COMO_DECODED_ROOTS = 256;
|
|
44
44
|
const MAX_WALK_NODES = 500_000;
|
|
45
|
+
const MAX_WALK_DEPTH = 128;
|
|
45
46
|
const RSC_FLIGHT_ROW = /(?:^|\n)(\d+):/u;
|
|
47
|
+
const PROFILE_ID = /^[A-Za-z0-9_-]{1,256}$/u;
|
|
48
|
+
const DISTANCE_IN_TEXT =
|
|
49
|
+
/(?:^|["'\\{,])(?:networkDistance|memberDistance|distance)"?\s*:\s*"?(DISTANCE_[A-Z0-9]+|OUT_OF_NETWORK|SELF|[0-9]+)"?/gu;
|
|
50
|
+
const PROFILE_URN_IN_TEXT = /urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}/gu;
|
|
51
|
+
const VIEWEE_PROFILE_ID_IN_TEXT = /vieweeProfileId"\s*:\s*"([A-Za-z0-9_-]{1,256})"/gu;
|
|
52
|
+
const VANITY_IN_TEXT =
|
|
53
|
+
/(?:vanityName|publicIdentifier)"\s*:\s*"([A-Za-z0-9][A-Za-z0-9_-]{1,99})"/gu;
|
|
54
|
+
const KEYED_IDENTITY_KEYS = new Set([
|
|
55
|
+
"distance",
|
|
56
|
+
"entityUrn",
|
|
57
|
+
"isSelfView",
|
|
58
|
+
"memberDistance",
|
|
59
|
+
"networkDistance",
|
|
60
|
+
"objectUrn",
|
|
61
|
+
"profileUrn",
|
|
62
|
+
"publicIdentifier",
|
|
63
|
+
"vanityName",
|
|
64
|
+
"vieweeMemberUrn",
|
|
65
|
+
"vieweeProfileId",
|
|
66
|
+
]);
|
|
46
67
|
const MAX_PHONES = 8;
|
|
47
68
|
const MAX_WEBSITES = 8;
|
|
48
69
|
const CONTACT_TYPE_SUFFIX = "ProfileContactInfo";
|
|
@@ -315,6 +336,99 @@ function looksLikeRscFlight(value: string): boolean {
|
|
|
315
336
|
return RSC_FLIGHT_ROW.test(value);
|
|
316
337
|
}
|
|
317
338
|
|
|
339
|
+
function extractJsonString(source: string, start: number): string | undefined {
|
|
340
|
+
if (source[start] !== "\"") return undefined;
|
|
341
|
+
let escaped = false;
|
|
342
|
+
const limit = Math.min(source.length, start + MAX_HTML_BYTES);
|
|
343
|
+
for (let index = start + 1; index < limit; index += 1) {
|
|
344
|
+
const character = source[index];
|
|
345
|
+
if (character === undefined) break;
|
|
346
|
+
if (escaped) {
|
|
347
|
+
escaped = false;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (character === "\\") {
|
|
351
|
+
escaped = true;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (character === "\"") return source.slice(start, index + 1);
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function breadcrumbRecordFromText(value: string): JsonRecord | null {
|
|
360
|
+
if (value.length < 8 || value.length > 1024 * 1024) return null;
|
|
361
|
+
const distances = [...value.matchAll(DISTANCE_IN_TEXT)]
|
|
362
|
+
.map((match) => match[1])
|
|
363
|
+
.filter((item): item is string => item !== undefined);
|
|
364
|
+
const urns = [...value.matchAll(PROFILE_URN_IN_TEXT)];
|
|
365
|
+
const vieweeIds = [...value.matchAll(VIEWEE_PROFILE_ID_IN_TEXT)]
|
|
366
|
+
.map((match) => match[1])
|
|
367
|
+
.filter((item): item is string => item !== undefined);
|
|
368
|
+
const vanities = [...value.matchAll(VANITY_IN_TEXT)]
|
|
369
|
+
.map((match) => match[1])
|
|
370
|
+
.filter((item): item is string => item !== undefined);
|
|
371
|
+
if (
|
|
372
|
+
distances.length === 0
|
|
373
|
+
&& urns.length === 0
|
|
374
|
+
&& vieweeIds.length === 0
|
|
375
|
+
&& vanities.length === 0
|
|
376
|
+
) return null;
|
|
377
|
+
const record: Record<string, unknown> = {};
|
|
378
|
+
if (distances.length === 1) {
|
|
379
|
+
const raw = distances[0]!;
|
|
380
|
+
record.networkDistance = /^[0-9]+$/u.test(raw) ? Number(raw) : raw;
|
|
381
|
+
} else if (distances.length > 1) {
|
|
382
|
+
const unique = [...new Set(distances)];
|
|
383
|
+
if (unique.length === 1) {
|
|
384
|
+
const raw = unique[0]!;
|
|
385
|
+
record.networkDistance = /^[0-9]+$/u.test(raw) ? Number(raw) : raw;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (urns.length === 1) record.profileUrn = urns[0]![0];
|
|
389
|
+
if (vieweeIds.length === 1) record.vieweeProfileId = vieweeIds[0];
|
|
390
|
+
if (vanities.length === 1) record.vanityName = vanities[0];
|
|
391
|
+
return Object.keys(record).length > 0 ? Object.freeze(record) : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function stringLooksLikeBootstrap(value: string): boolean {
|
|
395
|
+
return value.includes("networkDistance")
|
|
396
|
+
|| value.includes("memberDistance")
|
|
397
|
+
|| value.includes("vieweeProfileId")
|
|
398
|
+
|| value.includes("vieweeMemberUrn")
|
|
399
|
+
|| value.includes("vanityName")
|
|
400
|
+
|| value.includes("publicIdentifier")
|
|
401
|
+
|| /"distance"\s*:/u.test(value);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function decodeStringBootstrap(value: string): unknown[] {
|
|
405
|
+
const trimmed = value.trim();
|
|
406
|
+
if (trimmed.length < 8 || trimmed.length > 1024 * 1024) return [];
|
|
407
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
408
|
+
try {
|
|
409
|
+
return [JSON.parse(trimmed) as unknown];
|
|
410
|
+
} catch {
|
|
411
|
+
// Fall through to breadcrumb extraction from partial RSC string rows.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!stringLooksLikeBootstrap(trimmed)) return [];
|
|
415
|
+
const breadcrumb = breadcrumbRecordFromText(trimmed);
|
|
416
|
+
return breadcrumb === null ? [] : [breadcrumb];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function recordFromKeyedArray(value: readonly unknown[]): JsonRecord | null {
|
|
420
|
+
if (value.length < 2 || value.length % 2 !== 0 || value.length > 64) return null;
|
|
421
|
+
const record: Record<string, unknown> = {};
|
|
422
|
+
let recognized = 0;
|
|
423
|
+
for (let index = 0; index < value.length; index += 2) {
|
|
424
|
+
const key = value[index];
|
|
425
|
+
if (typeof key !== "string" || key.length < 1 || key.length > 128) return null;
|
|
426
|
+
record[key] = value[index + 1];
|
|
427
|
+
if (KEYED_IDENTITY_KEYS.has(key)) recognized += 1;
|
|
428
|
+
}
|
|
429
|
+
return recognized > 0 ? Object.freeze(record) : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
318
432
|
function decodeRscFlightRecords(text: string): unknown[] {
|
|
319
433
|
const records: unknown[] = [];
|
|
320
434
|
let index = 0;
|
|
@@ -334,7 +448,28 @@ function decodeRscFlightRecords(text: string): unknown[] {
|
|
|
334
448
|
index += json.length;
|
|
335
449
|
continue;
|
|
336
450
|
}
|
|
337
|
-
|
|
451
|
+
if (first === "\"") {
|
|
452
|
+
const json = extractJsonString(text, index);
|
|
453
|
+
if (json === undefined) {
|
|
454
|
+
index += 1;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const decoded = JSON.parse(json) as unknown;
|
|
459
|
+
if (typeof decoded === "string") records.push(...decodeStringBootstrap(decoded));
|
|
460
|
+
else records.push(decoded);
|
|
461
|
+
} catch {
|
|
462
|
+
const breadcrumb = breadcrumbRecordFromText(json);
|
|
463
|
+
if (breadcrumb !== null) records.push(breadcrumb);
|
|
464
|
+
}
|
|
465
|
+
index += json.length;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const remainder = text.slice(index);
|
|
469
|
+
const nextRow = remainder.search(/\n\d+:/u);
|
|
470
|
+
const rowText = nextRow === -1 ? remainder : remainder.slice(0, nextRow);
|
|
471
|
+
const breadcrumb = breadcrumbRecordFromText(rowText);
|
|
472
|
+
if (breadcrumb !== null) records.push(breadcrumb);
|
|
338
473
|
index = nextRow === -1 ? text.length : index + nextRow + 1;
|
|
339
474
|
}
|
|
340
475
|
return records;
|
|
@@ -433,13 +568,21 @@ function embeddedRecords(html: unknown): readonly JsonRecord[] {
|
|
|
433
568
|
while (stack.length > 0) {
|
|
434
569
|
const next = stack.pop()!;
|
|
435
570
|
nodes += 1;
|
|
436
|
-
if (nodes > MAX_WALK_NODES || next.depth >
|
|
571
|
+
if (nodes > MAX_WALK_NODES || next.depth > MAX_WALK_DEPTH) {
|
|
437
572
|
throw new Error("LinkedIn contact-info bootstrap exceeded its traversal bound");
|
|
438
573
|
}
|
|
574
|
+
if (typeof next.value === "string") {
|
|
575
|
+
for (const value of decodeStringBootstrap(next.value)) {
|
|
576
|
+
stack.push({ value, depth: next.depth + 1 });
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
439
580
|
if (Array.isArray(next.value)) {
|
|
440
581
|
if (next.value.length > 20_000) {
|
|
441
582
|
throw new Error("LinkedIn contact-info bootstrap array exceeded its reviewed bound");
|
|
442
583
|
}
|
|
584
|
+
const keyed = recordFromKeyedArray(next.value);
|
|
585
|
+
if (keyed !== null) records.push(keyed);
|
|
443
586
|
for (const value of next.value) stack.push({ value, depth: next.depth + 1 });
|
|
444
587
|
continue;
|
|
445
588
|
}
|
|
@@ -505,17 +648,33 @@ function vanityFromRecord(record: JsonRecord): string | null {
|
|
|
505
648
|
?? vanityFromHref(record.navigationUrl);
|
|
506
649
|
}
|
|
507
650
|
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (typeof value !== "string") continue;
|
|
651
|
+
function profileUrnFromIdentity(value: unknown): string | null {
|
|
652
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 512) return null;
|
|
653
|
+
if (value.startsWith("urn:li:fsd_profile:")) {
|
|
512
654
|
try {
|
|
513
655
|
return profileUrn(value);
|
|
514
656
|
} catch {
|
|
515
|
-
|
|
657
|
+
return null;
|
|
516
658
|
}
|
|
517
659
|
}
|
|
518
|
-
return null;
|
|
660
|
+
if (!PROFILE_ID.test(value)) return null;
|
|
661
|
+
try {
|
|
662
|
+
return profileUrn(`urn:li:fsd_profile:${value}`);
|
|
663
|
+
} catch {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function profileUrnFromRecord(record: JsonRecord): string | null {
|
|
669
|
+
for (const key of ["entityUrn", "objectUrn", "profileUrn", "vieweeMemberUrn"] as const) {
|
|
670
|
+
const urn = profileUrnFromIdentity(record[key]);
|
|
671
|
+
if (urn !== null) return urn;
|
|
672
|
+
}
|
|
673
|
+
return profileUrnFromIdentity(record.vieweeProfileId);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function recordIsSelfView(record: JsonRecord): boolean {
|
|
677
|
+
return record.isSelfView === true;
|
|
519
678
|
}
|
|
520
679
|
|
|
521
680
|
function selfProfileError(): Error {
|
|
@@ -524,6 +683,68 @@ function selfProfileError(): Error {
|
|
|
524
683
|
);
|
|
525
684
|
}
|
|
526
685
|
|
|
686
|
+
function omittedDistanceError(): Error {
|
|
687
|
+
return new Error(
|
|
688
|
+
"LinkedIn contact-info profile page omitted or contradicted its relationship distance",
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function uniqueDistances(values: readonly (string | number)[]): (string | number)[] {
|
|
693
|
+
const unique = [...new Set(values.map((value) => String(value)))];
|
|
694
|
+
return unique.map((value) => values.find((candidate) => String(candidate) === value)!);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function recordMatchesTarget(
|
|
698
|
+
record: JsonRecord,
|
|
699
|
+
slug: string,
|
|
700
|
+
urn: string,
|
|
701
|
+
): boolean {
|
|
702
|
+
const vanity = vanityFromRecord(record);
|
|
703
|
+
const recordUrn = profileUrnFromRecord(record);
|
|
704
|
+
return vanity === slug || recordUrn === urn;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function recordContradictsTarget(
|
|
708
|
+
record: JsonRecord,
|
|
709
|
+
slug: string,
|
|
710
|
+
urn: string,
|
|
711
|
+
): boolean {
|
|
712
|
+
const vanity = vanityFromRecord(record);
|
|
713
|
+
const recordUrn = profileUrnFromRecord(record);
|
|
714
|
+
return (vanity !== null && vanity !== slug) || (recordUrn !== null && recordUrn !== urn);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function relationshipDistance(
|
|
718
|
+
records: readonly JsonRecord[],
|
|
719
|
+
slug: string,
|
|
720
|
+
urn: string,
|
|
721
|
+
viewer: string,
|
|
722
|
+
): string | number {
|
|
723
|
+
const bound = records
|
|
724
|
+
.filter((record) => recordMatchesTarget(record, slug, urn))
|
|
725
|
+
.map(distanceValue)
|
|
726
|
+
.filter((value): value is string | number => value !== null);
|
|
727
|
+
const boundOther = bound.filter((value) => !isSelfDistance(value));
|
|
728
|
+
const boundUnique = uniqueDistances(boundOther);
|
|
729
|
+
if (boundUnique.length === 1 && boundUnique[0] !== undefined) return boundUnique[0];
|
|
730
|
+
if (boundUnique.length > 1) throw omittedDistanceError();
|
|
731
|
+
if (bound.some(isSelfDistance) && urn === viewer) throw selfProfileError();
|
|
732
|
+
|
|
733
|
+
const breadcrumbs = records
|
|
734
|
+
.filter((record) => !recordContradictsTarget(record, slug, urn))
|
|
735
|
+
.map(distanceValue)
|
|
736
|
+
.filter((value): value is string | number => value !== null);
|
|
737
|
+
const breadcrumbOther = breadcrumbs.filter((value) => !isSelfDistance(value));
|
|
738
|
+
const breadcrumbUnique = uniqueDistances(breadcrumbOther);
|
|
739
|
+
if (breadcrumbUnique.length === 1 && breadcrumbUnique[0] !== undefined) {
|
|
740
|
+
return breadcrumbUnique[0];
|
|
741
|
+
}
|
|
742
|
+
if (breadcrumbs.some(isSelfDistance) && (urn === viewer || breadcrumbOther.length === 0)) {
|
|
743
|
+
throw selfProfileError();
|
|
744
|
+
}
|
|
745
|
+
throw omittedDistanceError();
|
|
746
|
+
}
|
|
747
|
+
|
|
527
748
|
export function projectLinkedInProfileContactBinding(input: {
|
|
528
749
|
readonly profileHtml: unknown;
|
|
529
750
|
readonly profileUrl: unknown;
|
|
@@ -536,6 +757,7 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
536
757
|
if (vanityRecords.length < 1) {
|
|
537
758
|
throw new Error("LinkedIn contact-info profile page did not bind the requested vanity");
|
|
538
759
|
}
|
|
760
|
+
if (vanityRecords.some(recordIsSelfView)) throw selfProfileError();
|
|
539
761
|
const urns = new Set<string>();
|
|
540
762
|
for (const record of vanityRecords) {
|
|
541
763
|
const urn = profileUrnFromRecord(record);
|
|
@@ -547,7 +769,11 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
547
769
|
for (const record of records) {
|
|
548
770
|
const urn = profileUrnFromRecord(record);
|
|
549
771
|
const distance = distanceValue(record);
|
|
550
|
-
if (
|
|
772
|
+
if (
|
|
773
|
+
recordIsSelfView(record)
|
|
774
|
+
|| urn === viewer
|
|
775
|
+
|| (distance !== null && isSelfDistance(distance))
|
|
776
|
+
) {
|
|
551
777
|
sawSelf = true;
|
|
552
778
|
}
|
|
553
779
|
if (urn === null || urn === viewer || distance === null) continue;
|
|
@@ -567,17 +793,7 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
567
793
|
}
|
|
568
794
|
const urn = urns.values().next().value!;
|
|
569
795
|
if (urn === viewer) throw selfProfileError();
|
|
570
|
-
const
|
|
571
|
-
vanityFromRecord(record) === target.slug || profileUrnFromRecord(record) === urn
|
|
572
|
-
);
|
|
573
|
-
const distances = related
|
|
574
|
-
.map(distanceValue)
|
|
575
|
-
.filter((value): value is string | number => value !== null);
|
|
576
|
-
const unique = [...new Set(distances.map((value) => String(value)))];
|
|
577
|
-
if (unique.length !== 1 || distances[0] === undefined) {
|
|
578
|
-
throw new Error("LinkedIn contact-info profile page omitted or contradicted its relationship distance");
|
|
579
|
-
}
|
|
580
|
-
const distance = distances[0];
|
|
796
|
+
const distance = relationshipDistance(records, target.slug, urn, viewer);
|
|
581
797
|
if (isSelfDistance(distance)) throw selfProfileError();
|
|
582
798
|
if (!isFirstDegree(distance)) {
|
|
583
799
|
throw new Error(
|
|
@@ -826,7 +1042,7 @@ function collectRecords(value: unknown): readonly JsonRecord[] {
|
|
|
826
1042
|
while (stack.length > 0) {
|
|
827
1043
|
const next = stack.pop()!;
|
|
828
1044
|
nodes += 1;
|
|
829
|
-
if (nodes > MAX_WALK_NODES || next.depth >
|
|
1045
|
+
if (nodes > MAX_WALK_NODES || next.depth > MAX_WALK_DEPTH) {
|
|
830
1046
|
throw new Error("LinkedIn contact-info payload exceeded its traversal bound");
|
|
831
1047
|
}
|
|
832
1048
|
if (Array.isArray(next.value)) {
|
package/src/read-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalJson, sha256 } from "./canonical-json";
|
|
2
|
+
import { assertOperationPermission, withOperationPermission } from "./operation-permission";
|
|
2
3
|
import { loadAuthSnapshotIfPresent } from "./auth";
|
|
3
4
|
import {
|
|
4
5
|
createPortableProviderPluginCatalog,
|
|
@@ -239,8 +240,9 @@ export function readCachedPreparedCapability(
|
|
|
239
240
|
options: ReadCapabilityOptions & { readonly registry: ProviderPluginRegistry },
|
|
240
241
|
): ReadProjectionCacheResult {
|
|
241
242
|
const environment = options.environment ?? process.env;
|
|
243
|
+
assertOperationPermission(invocation, { environment, registry: options.registry });
|
|
242
244
|
validateReadOptions(options);
|
|
243
|
-
|
|
245
|
+
const result = withInvocationAuthorityAdmission(
|
|
244
246
|
invocation,
|
|
245
247
|
environment,
|
|
246
248
|
() => {
|
|
@@ -265,6 +267,8 @@ export function readCachedPreparedCapability(
|
|
|
265
267
|
return cached;
|
|
266
268
|
},
|
|
267
269
|
);
|
|
270
|
+
assertOperationPermission(invocation, { environment, registry: options.registry });
|
|
271
|
+
return result;
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
export function readCachedCapability(
|
|
@@ -279,7 +283,7 @@ export function readCachedCapability(
|
|
|
279
283
|
);
|
|
280
284
|
}
|
|
281
285
|
|
|
282
|
-
|
|
286
|
+
async function revalidatePreparedCapabilityCore(
|
|
283
287
|
invocation: PreparedInvocation,
|
|
284
288
|
options: PreparedReadOptions,
|
|
285
289
|
cachedBeforeOverride?: ReadProjectionCacheResult | null,
|
|
@@ -467,6 +471,12 @@ export async function revalidatePreparedCapability(
|
|
|
467
471
|
}
|
|
468
472
|
}
|
|
469
473
|
|
|
474
|
+
export async function revalidatePreparedCapability(invocation: PreparedInvocation, options: PreparedReadOptions, cachedBeforeOverride?: ReadProjectionCacheResult | null): Promise<RevalidatedCapability> {
|
|
475
|
+
const environment = options.environment ?? process.env;
|
|
476
|
+
return withOperationPermission(invocation, { environment, registry: options.registry, ...(options.signal === undefined ? {} : { signal: options.signal }) },
|
|
477
|
+
() => revalidatePreparedCapabilityCore(invocation, options, cachedBeforeOverride));
|
|
478
|
+
}
|
|
479
|
+
|
|
470
480
|
export async function revalidateCapability(
|
|
471
481
|
request: CapabilityReadRequest,
|
|
472
482
|
options: RevalidateCapabilityOptions = {},
|
package/src/runtime.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
+
import { assertOperationPreparationPermission, checkOperationPermission, withOperationPermission, withUnmanagedOperationPermission, readOperationPolicy } from "./operation-permission";
|
|
3
|
+
import type { ApprovalTarget } from "./control/protocol";
|
|
2
4
|
import { ConfirmedWritePlatform, makeConfirmedWritePlatform } from "./confirmed-write-platform";
|
|
3
5
|
import { confirmedWriteProgram } from "./confirmed-write-program";
|
|
4
6
|
import { runConfirmedWrite } from "./confirmed-write-runtime";
|
|
@@ -186,6 +188,7 @@ type InvocationPlanCommon = {
|
|
|
186
188
|
readonly id: string;
|
|
187
189
|
readonly hash: string;
|
|
188
190
|
readonly kind: GhostgetAuth["kind"];
|
|
191
|
+
readonly incarnationHash?: string;
|
|
189
192
|
};
|
|
190
193
|
readonly duplicateRisk?: InvocationDuplicateRiskV1;
|
|
191
194
|
readonly messagingComposite?: MessagingCompositeInvocationPlanV1;
|
|
@@ -1231,6 +1234,7 @@ export function prepareInvocation(
|
|
|
1231
1234
|
authId?: string,
|
|
1232
1235
|
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
1233
1236
|
registry: ProviderPluginRegistry = providerPluginRegistry,
|
|
1237
|
+
permissionMode: "enforce" | "inspect" = "enforce",
|
|
1234
1238
|
): PreparedInvocation {
|
|
1235
1239
|
const manifestResult = loadInstalledManifestWithRegistry(adapterId, environment, registry);
|
|
1236
1240
|
if (!manifestResult.ok) throw new Error(`adapter ${adapterId} is invalid: ${manifestResult.issues.join("; ")}`);
|
|
@@ -1257,7 +1261,7 @@ export function prepareInvocation(
|
|
|
1257
1261
|
);
|
|
1258
1262
|
}
|
|
1259
1263
|
const authority = authenticationPolicy.authority;
|
|
1260
|
-
|
|
1264
|
+
const invocation = revalidatePreparedInvocation({
|
|
1261
1265
|
manifest: manifestResult.value,
|
|
1262
1266
|
operationId,
|
|
1263
1267
|
input: platformInput.value,
|
|
@@ -1265,6 +1269,8 @@ export function prepareInvocation(
|
|
|
1265
1269
|
readProjectionAuthIdentityHash:
|
|
1266
1270
|
publicWebSessionAuthorityIdentityHash(authority),
|
|
1267
1271
|
}, registry).invocation;
|
|
1272
|
+
if (permissionMode === "enforce") assertOperationPreparationPermission(invocation, { environment, registry });
|
|
1273
|
+
return invocation;
|
|
1268
1274
|
}
|
|
1269
1275
|
const selectedAuthId = authId ?? adapterId;
|
|
1270
1276
|
const preparedAuth = withSettledReadProjectionAuthAdmission(
|
|
@@ -1282,7 +1288,7 @@ export function prepareInvocation(
|
|
|
1282
1288
|
});
|
|
1283
1289
|
},
|
|
1284
1290
|
);
|
|
1285
|
-
|
|
1291
|
+
const invocation = revalidatePreparedInvocation({
|
|
1286
1292
|
manifest: manifestResult.value,
|
|
1287
1293
|
operationId,
|
|
1288
1294
|
input: platformInput.value,
|
|
@@ -1290,6 +1296,8 @@ export function prepareInvocation(
|
|
|
1290
1296
|
readProjectionAuthIdentityHash:
|
|
1291
1297
|
preparedAuth.readProjectionAuthIdentityHash,
|
|
1292
1298
|
}, registry).invocation;
|
|
1299
|
+
if (permissionMode === "enforce") assertOperationPreparationPermission(invocation, { environment, registry });
|
|
1300
|
+
return invocation;
|
|
1293
1301
|
}
|
|
1294
1302
|
|
|
1295
1303
|
/**
|
|
@@ -1448,6 +1456,7 @@ export function createInvocationPlan(
|
|
|
1448
1456
|
id: planAuth.id,
|
|
1449
1457
|
hash: authHash(planAuth),
|
|
1450
1458
|
kind: planAuth.kind,
|
|
1459
|
+
...(invocation.readProjectionAuthIdentityHash === undefined ? {} : { incarnationHash: invocation.readProjectionAuthIdentityHash }),
|
|
1451
1460
|
},
|
|
1452
1461
|
};
|
|
1453
1462
|
const portablePluginContract = pluginResolution?.portableIdentity ?? null;
|
|
@@ -1678,6 +1687,12 @@ function planRunJournalContract(plan: InvocationPlan): RunJournal["contract"] {
|
|
|
1678
1687
|
: { transport: contract.transport, hash: contract.hash };
|
|
1679
1688
|
}
|
|
1680
1689
|
|
|
1690
|
+
/** Recovery v1 records the durable account selection. The full lifetime binding
|
|
1691
|
+
* remains in the successor scope hash and is revalidated before confirmation. */
|
|
1692
|
+
function planRecoveryAuth(plan: InvocationPlan): RunJournal["auth"] {
|
|
1693
|
+
return { id: plan.auth.id, hash: plan.auth.hash, kind: plan.auth.kind };
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1681
1696
|
function planFileInputs(input: OperationInput): readonly FileInputValue[] {
|
|
1682
1697
|
const files: FileInputValue[] = [];
|
|
1683
1698
|
for (const value of Object.values(input)) {
|
|
@@ -1753,7 +1768,7 @@ function resolveInvocationDuplicateRisk(
|
|
|
1753
1768
|
|| journal.operation !== plan.operation
|
|
1754
1769
|
|| journal.risk !== plan.risk
|
|
1755
1770
|
|| journal.inputHash !== plan.inputHash
|
|
1756
|
-
|| canonicalJson(journal.auth) !== canonicalJson(plan
|
|
1771
|
+
|| canonicalJson(journal.auth) !== canonicalJson(planRecoveryAuth(plan))
|
|
1757
1772
|
|| canonicalJson(journal.contract)
|
|
1758
1773
|
!== canonicalJson(planRunJournalContract(plan))
|
|
1759
1774
|
) {
|
|
@@ -1786,7 +1801,7 @@ function resolveInvocationDuplicateRisk(
|
|
|
1786
1801
|
|| capsule.risk !== plan.risk
|
|
1787
1802
|
|| capsule.inputHash !== plan.inputHash
|
|
1788
1803
|
|| canonicalJson(capsule.input) !== canonicalJson(plan.input)
|
|
1789
|
-
|| canonicalJson(capsule.auth) !== canonicalJson(plan
|
|
1804
|
+
|| canonicalJson(capsule.auth) !== canonicalJson(planRecoveryAuth(plan))
|
|
1790
1805
|
|| canonicalJson(capsule.contract) !== canonicalJson(planRecoveryContract(plan))
|
|
1791
1806
|
) {
|
|
1792
1807
|
throw new Error(
|
|
@@ -2433,7 +2448,8 @@ function parseStoredPlan(value: unknown): StoredPlan {
|
|
|
2433
2448
|
) throw new Error("stored plan adapter is malformed");
|
|
2434
2449
|
const auth = raw.auth;
|
|
2435
2450
|
if (
|
|
2436
|
-
!hasExactKeys(auth, ["id", "hash", "kind"])
|
|
2451
|
+
!hasExactKeys(auth, ["id", "hash", "kind", ...(Object.hasOwn(auth, "incarnationHash") ? ["incarnationHash"] : [])])
|
|
2452
|
+
|| (Object.hasOwn(auth, "incarnationHash") && (typeof auth.incarnationHash !== "string" || !/^[a-f0-9]{64}$/u.test(auth.incarnationHash)))
|
|
2437
2453
|
|| typeof auth.id !== "string"
|
|
2438
2454
|
|| !/^[a-z][a-z0-9-]{0,47}$/u.test(auth.id)
|
|
2439
2455
|
|| typeof auth.hash !== "string"
|
|
@@ -2550,7 +2566,7 @@ function parseStoredPlan(value: unknown): StoredPlan {
|
|
|
2550
2566
|
input,
|
|
2551
2567
|
inputHash,
|
|
2552
2568
|
dispatches,
|
|
2553
|
-
auth: { id: auth.id, hash: auth.hash, kind: auth.kind },
|
|
2569
|
+
auth: { id: auth.id, hash: auth.hash, kind: auth.kind, ...(typeof auth.incarnationHash === "string" ? { incarnationHash: auth.incarnationHash } : {}) },
|
|
2554
2570
|
...(Object.hasOwn(raw, "duplicateRisk")
|
|
2555
2571
|
? { duplicateRisk: parseInvocationDuplicateRisk(raw.duplicateRisk) }
|
|
2556
2572
|
: {}),
|
|
@@ -2977,14 +2993,41 @@ function validateFreshPlan(
|
|
|
2977
2993
|
} else if (auth.kind === "oauth-token-file") {
|
|
2978
2994
|
throw new Error("browser authentication changed after preview; preview the action again");
|
|
2979
2995
|
}
|
|
2996
|
+
const incarnation = withSettledReadProjectionAuthAdmission(auth.id, environment, () => {
|
|
2997
|
+
const current = loadAuth(auth.id, environment);
|
|
2998
|
+
if (authHash(current) !== authHash(auth)) throw new Error("authentication changed during confirmation preparation");
|
|
2999
|
+
return projectionAuthIdentityHash(auth.id, authHash(auth), environment);
|
|
3000
|
+
});
|
|
3001
|
+
if ((plan.auth.incarnationHash !== undefined && plan.auth.incarnationHash !== incarnation)
|
|
3002
|
+
|| (plan.auth.incarnationHash === undefined && readOperationPolicy(environment).managed)) {
|
|
3003
|
+
throw new Error("authentication lifetime changed or predates managed permissions; preview the action again");
|
|
3004
|
+
}
|
|
2980
3005
|
return revalidatePreparedInvocation({
|
|
2981
3006
|
manifest,
|
|
2982
3007
|
operationId: plan.operation,
|
|
2983
3008
|
input: platformInput.value,
|
|
2984
3009
|
auth,
|
|
3010
|
+
readProjectionAuthIdentityHash: incarnation,
|
|
2985
3011
|
}, registry).invocation;
|
|
2986
3012
|
}
|
|
2987
3013
|
|
|
3014
|
+
/** Trusted control-plane inspection. This only prepares; execution gates remain mandatory. */
|
|
3015
|
+
export function prepareOperationApprovalInvocation(
|
|
3016
|
+
target: Extract<ApprovalTarget, { readonly kind: "provider" }>,
|
|
3017
|
+
options: { readonly environment: Readonly<Record<string, string | undefined>>; readonly registry: ProviderPluginRegistry },
|
|
3018
|
+
): { readonly invocation: PreparedInvocation; readonly stored: StoredPlan | null } {
|
|
3019
|
+
if (target.planDigest === null) return {
|
|
3020
|
+
invocation: prepareInvocation(target.adapterId, target.operationId, target.input, target.authId ?? undefined, options.environment, options.registry, "inspect"), stored: null,
|
|
3021
|
+
};
|
|
3022
|
+
const stored = loadInvocationPlan(target.planDigest, options.environment);
|
|
3023
|
+
if (stored.plan.adapter.id !== target.adapterId || stored.plan.operation !== target.operationId || stored.plan.auth.id !== target.authId) {
|
|
3024
|
+
throw new Error("approval target does not match its exact saved plan");
|
|
3025
|
+
}
|
|
3026
|
+
const invocation = validateFreshPlan(stored, options.environment, new Date(), options.registry,
|
|
3027
|
+
(id, environment = options.environment) => loadInstalledManifestWithRegistry(id, environment, options.registry));
|
|
3028
|
+
return { invocation, stored };
|
|
3029
|
+
}
|
|
3030
|
+
|
|
2988
3031
|
|
|
2989
3032
|
|
|
2990
3033
|
|
|
@@ -4381,6 +4424,7 @@ async function runPreparedReadCore(invocation: PreparedInvocation, planDigest: s
|
|
|
4381
4424
|
|
|
4382
4425
|
},
|
|
4383
4426
|
execute: async () => {
|
|
4427
|
+
await checkOperationPermission(invocation, { environment: options.environment, registry, ...(options.signal === undefined ? {} : { signal: options.signal }) });
|
|
4384
4428
|
if (options.preflightFailure !== undefined) throw options.preflightFailure;
|
|
4385
4429
|
return providerOperation
|
|
4386
4430
|
? await (options.executeProvider ?? executeProviderOperation)(
|
|
@@ -4524,7 +4568,7 @@ async function runPreparedReadCore(invocation: PreparedInvocation, planDigest: s
|
|
|
4524
4568
|
|
|
4525
4569
|
|
|
4526
4570
|
|
|
4527
|
-
async function
|
|
4571
|
+
async function runPreparedCore(
|
|
4528
4572
|
invocation: PreparedInvocation,
|
|
4529
4573
|
planDigest: string | null,
|
|
4530
4574
|
options: RunPreparedOptions,
|
|
@@ -4593,6 +4637,12 @@ async function runPrepared(
|
|
|
4593
4637
|
);
|
|
4594
4638
|
}
|
|
4595
4639
|
|
|
4640
|
+
async function runPrepared(invocation: PreparedInvocation, planDigest: string | null, options: RunPreparedOptions): Promise<InvocationResult> {
|
|
4641
|
+
if (!readOperationPolicy(options.environment).managed) return withUnmanagedOperationPermission(options.environment, () => runPreparedCore(invocation, planDigest, options));
|
|
4642
|
+
return withOperationPermission(invocation, { environment: options.environment, registry: options.registry ?? providerPluginRegistry,
|
|
4643
|
+
...(options.signal === undefined ? {} : { signal: options.signal }) }, () => runPreparedCore(invocation, planDigest, options));
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4596
4646
|
export async function executeReadInvocation(
|
|
4597
4647
|
invocation: PreparedInvocation,
|
|
4598
4648
|
options: {
|
|
@@ -4628,7 +4678,7 @@ export async function executeReadInvocation(
|
|
|
4628
4678
|
});
|
|
4629
4679
|
}
|
|
4630
4680
|
|
|
4631
|
-
|
|
4681
|
+
async function confirmInvocationCore(
|
|
4632
4682
|
digest: string,
|
|
4633
4683
|
options: {
|
|
4634
4684
|
readonly headed: boolean;
|
|
@@ -4681,6 +4731,17 @@ export async function confirmInvocation(
|
|
|
4681
4731
|
}, options))));
|
|
4682
4732
|
}
|
|
4683
4733
|
|
|
4734
|
+
export async function confirmInvocation(digest: string, options: Parameters<typeof confirmInvocationCore>[1]): Promise<InvocationResult> {
|
|
4735
|
+
const environment = options.environment ?? process.env;
|
|
4736
|
+
if (!readOperationPolicy(environment).managed) return withUnmanagedOperationPermission(environment, () => confirmInvocationCore(digest, options));
|
|
4737
|
+
const registry = options.registry ?? providerPluginRegistry;
|
|
4738
|
+
const stored = loadInvocationPlan(digest, environment);
|
|
4739
|
+
const invocation = validateFreshPlan(stored, environment, options.now ?? new Date(), registry,
|
|
4740
|
+
options.loadManifest ?? ((id, selected = environment) => loadInstalledManifestWithRegistry(id, selected, registry)));
|
|
4741
|
+
return withOperationPermission(invocation, { environment, registry, plan: stored, ...(options.signal === undefined ? {} : { signal: options.signal }) },
|
|
4742
|
+
() => confirmInvocationCore(digest, options));
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4684
4745
|
export type MessagingConfirmationResult = {
|
|
4685
4746
|
readonly run: MessagingRunV1;
|
|
4686
4747
|
readonly receipt: MessagingRunReceipt;
|
|
@@ -4728,7 +4789,7 @@ function terminalizeMessagingRecovery(
|
|
|
4728
4789
|
}
|
|
4729
4790
|
|
|
4730
4791
|
/** Confirm one composite messaging preview under one durable ownership claim. */
|
|
4731
|
-
|
|
4792
|
+
async function confirmMessagingInvocationCore(
|
|
4732
4793
|
digest: string,
|
|
4733
4794
|
options: {
|
|
4734
4795
|
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
@@ -4857,6 +4918,17 @@ export async function confirmMessagingInvocation(
|
|
|
4857
4918
|
}
|
|
4858
4919
|
}
|
|
4859
4920
|
|
|
4921
|
+
export async function confirmMessagingInvocation(digest: string, options: NonNullable<Parameters<typeof confirmMessagingInvocationCore>[1]> = {}): Promise<MessagingConfirmationResult> {
|
|
4922
|
+
const environment = options.environment ?? process.env;
|
|
4923
|
+
if (!readOperationPolicy(environment).managed) return withUnmanagedOperationPermission(environment, () => confirmMessagingInvocationCore(digest, options));
|
|
4924
|
+
const registry = options.registry ?? providerPluginRegistry;
|
|
4925
|
+
const stored = loadInvocationPlan(digest, environment);
|
|
4926
|
+
const invocation = validateFreshPlan(stored, environment, options.now ?? new Date(), registry,
|
|
4927
|
+
options.loadManifest ?? ((id, selected = environment) => loadInstalledManifestWithRegistry(id, selected, registry)));
|
|
4928
|
+
return withOperationPermission(invocation, { environment, registry, plan: stored, ...(options.signal === undefined ? {} : { signal: options.signal }) },
|
|
4929
|
+
() => confirmMessagingInvocationCore(digest, options));
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4860
4932
|
export function readRunReceipt(
|
|
4861
4933
|
runId: string,
|
|
4862
4934
|
environment: Readonly<Record<string, string | undefined>> = process.env,
|
package/src/state-helper.ts
CHANGED
|
@@ -41,11 +41,13 @@ const stateDirectories = new Set([
|
|
|
41
41
|
"auth",
|
|
42
42
|
"browser-snapshots",
|
|
43
43
|
"captures",
|
|
44
|
+
"control",
|
|
44
45
|
"derivations",
|
|
45
46
|
"idempotency",
|
|
46
47
|
"linked-device-stores",
|
|
47
48
|
"messaging",
|
|
48
49
|
"omni-read-projections",
|
|
50
|
+
"operation-permissions",
|
|
49
51
|
"plan-assets",
|
|
50
52
|
"plans",
|
|
51
53
|
"provider-plugin-state",
|