@evomap/evolver-adapter-public 2.0.0-beta.19 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/antiAbuseTelemetry.js +2 -1
- package/dist/auth/credentialStore.d.ts +5 -2
- package/dist/auth/credentialStore.js +133 -45
- package/dist/auth/legacyShim.d.ts +2 -2
- package/dist/auth/legacyShim.js +2 -2
- package/dist/auth/windowsPowerShell.d.ts +3 -0
- package/dist/auth/windowsPowerShell.js +91 -0
- package/dist/hubCapability.d.ts +14 -1
- package/dist/hubCapability.js +274 -33
- package/dist/hubFetch.d.ts +3 -3
- package/dist/hubFetch.js +55 -13
- package/dist/hubReuse.d.ts +2 -1
- package/dist/hubReuse.js +34 -19
- package/dist/learningPacketSink.d.ts +9 -1
- package/dist/learningPacketSink.js +51 -10
- package/package.json +2 -2
|
@@ -99,6 +99,7 @@ export function collectIntegrityHashes(packageRoot = resolveAdapterPackageRoot()
|
|
|
99
99
|
export function buildHeartbeatAntiAbuseTelemetry(opts = {}) {
|
|
100
100
|
const env = opts.env ?? process.env;
|
|
101
101
|
const fp = opts.envFingerprint ?? bootstrap.captureEnvFingerprint({ env });
|
|
102
|
+
const evolverVersion = bootstrap.normalizeEvolverVersion(Object.hasOwn(opts, 'evolverVersion') ? opts.evolverVersion : fp.evolver_version);
|
|
102
103
|
const salt = opts.salt ?? env['EVOLVER_ANTI_ABUSE_SALT'];
|
|
103
104
|
const saltId = opts.saltId ?? env['EVOLVER_ANTI_ABUSE_SALT_ID'] ?? (salt ? 'env' : null);
|
|
104
105
|
const pseudonymStatus = salt ? 'salt_configured' : 'salt_missing';
|
|
@@ -159,7 +160,7 @@ export function buildHeartbeatAntiAbuseTelemetry(opts = {}) {
|
|
|
159
160
|
// AUTHORITATIVE per-asset model lives on the capsule (threaded from input.model), so 'unknown' on a heartbeat
|
|
160
161
|
// is "this node didn't say", never a contradiction of a capsule's real model.
|
|
161
162
|
model: fp.model,
|
|
162
|
-
evolver_version:
|
|
163
|
+
evolver_version: evolverVersion ?? null,
|
|
163
164
|
client: '@evomap/evolver-adapter-public',
|
|
164
165
|
client_version: null,
|
|
165
166
|
region: fp.region ?? null,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { linkSync } from 'node:fs';
|
|
1
|
+
import { linkSync, renameSync } from 'node:fs';
|
|
2
2
|
import type { hub } from '@evomap/evolver-core';
|
|
3
3
|
export interface CredentialStoreOptions {
|
|
4
4
|
/**
|
|
@@ -15,6 +15,8 @@ export interface CredentialStoreOptions {
|
|
|
15
15
|
windowsParentStateReader?: (path: string) => readonly PathSecurityState[];
|
|
16
16
|
/** Injectable filesystem primitive for deterministic no-clobber publication tests. */
|
|
17
17
|
linkFile?: typeof linkSync;
|
|
18
|
+
/** Injectable filesystem primitive for deterministic atomic replacement tests. */
|
|
19
|
+
renameFile?: typeof renameSync;
|
|
18
20
|
}
|
|
19
21
|
export interface WindowsCredentialAclOps {
|
|
20
22
|
assertTrustedParent(path: string, strictCreate: boolean): void;
|
|
@@ -24,7 +26,7 @@ export interface WindowsCredentialAclOps {
|
|
|
24
26
|
secureFile(path: string): void;
|
|
25
27
|
}
|
|
26
28
|
export declare class CredentialStoreError extends Error {
|
|
27
|
-
constructor(message: string);
|
|
29
|
+
constructor(message: string, options?: ErrorOptions);
|
|
28
30
|
}
|
|
29
31
|
interface FileIdentity {
|
|
30
32
|
dev: bigint;
|
|
@@ -44,6 +46,7 @@ export declare class CredentialStore {
|
|
|
44
46
|
private readonly darwinAclReader;
|
|
45
47
|
private readonly windowsParentStateReader;
|
|
46
48
|
private readonly linkFile;
|
|
49
|
+
private readonly renameFile;
|
|
47
50
|
private securedDirectoryState;
|
|
48
51
|
private securedCredentialState;
|
|
49
52
|
private readonly securedAncestorStates;
|
|
@@ -2,11 +2,12 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { constants, closeSync, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, resolve, win32 } from 'node:path';
|
|
5
|
+
import { POWERSHELL_STDIN_SCRIPT_COMMAND, windowsAclFailureDetail, } from './windowsPowerShell.js';
|
|
5
6
|
const DIRECTORY_MODE = 0o700;
|
|
6
7
|
const FILE_MODE = 0o600;
|
|
7
8
|
export class CredentialStoreError extends Error {
|
|
8
|
-
constructor(message) {
|
|
9
|
-
super(`Unsafe credential path: ${message}
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(`Unsafe credential path: ${message}`, options);
|
|
10
11
|
this.name = 'CredentialStoreError';
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -18,6 +19,7 @@ export class CredentialStore {
|
|
|
18
19
|
darwinAclReader;
|
|
19
20
|
windowsParentStateReader;
|
|
20
21
|
linkFile;
|
|
22
|
+
renameFile;
|
|
21
23
|
// ctime detects in-place ACL drift while dev/ino detects entry replacement.
|
|
22
24
|
securedDirectoryState = null;
|
|
23
25
|
securedCredentialState = null;
|
|
@@ -32,6 +34,7 @@ export class CredentialStore {
|
|
|
32
34
|
this.darwinAclReader = options.darwinAclReader ?? readDarwinAcl;
|
|
33
35
|
this.windowsParentStateReader = options.windowsParentStateReader ?? parentSecurityStates;
|
|
34
36
|
this.linkFile = options.linkFile ?? linkSync;
|
|
37
|
+
this.renameFile = options.renameFile ?? renameSync;
|
|
35
38
|
}
|
|
36
39
|
load() {
|
|
37
40
|
if (!this.prepareDirectory(false))
|
|
@@ -152,7 +155,7 @@ export class CredentialStore {
|
|
|
152
155
|
this.assertDirectoryIdentity(directory, directoryIdentity);
|
|
153
156
|
if (replaceExisting) {
|
|
154
157
|
this.assertSafeDestination();
|
|
155
|
-
|
|
158
|
+
this.renameFile(temporaryPath, this.path);
|
|
156
159
|
}
|
|
157
160
|
else if (!this.publishIfAbsent(temporaryPath)) {
|
|
158
161
|
const incumbentFd = this.openCredentialFile();
|
|
@@ -392,8 +395,8 @@ export class CredentialStore {
|
|
|
392
395
|
try {
|
|
393
396
|
this.windowsAclOps.assertTrustedFile(this.path);
|
|
394
397
|
}
|
|
395
|
-
catch {
|
|
396
|
-
throw
|
|
398
|
+
catch (cause) {
|
|
399
|
+
throw windowsCredentialStoreError('Windows credential file ACL is not trusted', cause);
|
|
397
400
|
}
|
|
398
401
|
const pathStat = bigLstat(this.path);
|
|
399
402
|
const fdStat = bigFstat(fd);
|
|
@@ -511,8 +514,8 @@ export class CredentialStore {
|
|
|
511
514
|
try {
|
|
512
515
|
this.windowsAclOps.secureDirectory(path);
|
|
513
516
|
}
|
|
514
|
-
catch {
|
|
515
|
-
throw
|
|
517
|
+
catch (cause) {
|
|
518
|
+
throw windowsCredentialStoreError('Windows directory ACL could not be secured', cause);
|
|
516
519
|
}
|
|
517
520
|
const after = bigLstat(path);
|
|
518
521
|
if (after.isSymbolicLink() || !after.isDirectory() || !sameIdentity(after, identity)) {
|
|
@@ -532,8 +535,8 @@ export class CredentialStore {
|
|
|
532
535
|
try {
|
|
533
536
|
this.windowsAclOps.assertTrustedFile(path);
|
|
534
537
|
}
|
|
535
|
-
catch {
|
|
536
|
-
throw
|
|
538
|
+
catch (cause) {
|
|
539
|
+
throw windowsCredentialStoreError('Windows credential file ACL is not trusted', cause);
|
|
537
540
|
}
|
|
538
541
|
if (isCredentialPath && this.securedCredentialState &&
|
|
539
542
|
sameSecurityState(this.securedCredentialState, before))
|
|
@@ -541,8 +544,8 @@ export class CredentialStore {
|
|
|
541
544
|
try {
|
|
542
545
|
this.windowsAclOps.secureFile(path);
|
|
543
546
|
}
|
|
544
|
-
catch {
|
|
545
|
-
throw
|
|
547
|
+
catch (cause) {
|
|
548
|
+
throw windowsCredentialStoreError('Windows file ACL could not be secured', cause);
|
|
546
549
|
}
|
|
547
550
|
const pathStat = bigLstat(path);
|
|
548
551
|
const fdStat = bigFstat(fd);
|
|
@@ -595,7 +598,8 @@ export class CredentialStore {
|
|
|
595
598
|
throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
|
|
596
599
|
}
|
|
597
600
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
598
|
-
const
|
|
601
|
+
const initialMetadata = bigFstat(fd);
|
|
602
|
+
const initialState = securityStateOf(initialMetadata);
|
|
599
603
|
let output;
|
|
600
604
|
try {
|
|
601
605
|
output = this.darwinAclReader(path);
|
|
@@ -612,12 +616,33 @@ export class CredentialStore {
|
|
|
612
616
|
!sameIdentity(after, identity) || !sameIdentity(openedAfter, identity)) {
|
|
613
617
|
throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
|
|
614
618
|
}
|
|
615
|
-
|
|
616
|
-
sameSecurityState(initialState, openedAfter);
|
|
617
|
-
if (metadataStable) {
|
|
619
|
+
if (sameSecurityState(initialState, after) && sameSecurityState(initialState, openedAfter)) {
|
|
618
620
|
this.securedAncestorStates.set(path, securityStateOf(openedAfter));
|
|
619
621
|
return;
|
|
620
622
|
}
|
|
623
|
+
if (sameDarwinAncestorMetadata(initialMetadata, after)
|
|
624
|
+
&& sameDarwinAncestorMetadata(initialMetadata, openedAfter)) {
|
|
625
|
+
let confirmedOutput;
|
|
626
|
+
try {
|
|
627
|
+
confirmedOutput = this.darwinAclReader(path);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
throw new CredentialStoreError(`ancestor directory ${path} ACL could not be inspected`);
|
|
631
|
+
}
|
|
632
|
+
if (hasUnsafeDarwinAllowAcl(confirmedOutput, rejectAnyAllow)) {
|
|
633
|
+
throw new CredentialStoreError(`ancestor directory ${path} grants access through an extended ACL`);
|
|
634
|
+
}
|
|
635
|
+
const confirmedPath = bigLstat(path);
|
|
636
|
+
const confirmedOpened = bigFstat(fd);
|
|
637
|
+
if (confirmedOutput === output
|
|
638
|
+
&& !confirmedPath.isSymbolicLink()
|
|
639
|
+
&& confirmedPath.isDirectory()
|
|
640
|
+
&& sameDarwinAncestorMetadata(initialMetadata, confirmedPath)
|
|
641
|
+
&& sameDarwinAncestorMetadata(initialMetadata, confirmedOpened)) {
|
|
642
|
+
this.securedAncestorStates.set(path, securityStateOf(confirmedOpened));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
621
646
|
}
|
|
622
647
|
throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
|
|
623
648
|
}
|
|
@@ -667,16 +692,58 @@ export class CredentialStore {
|
|
|
667
692
|
this.trustedWindowsParentStates.set(cacheKey, after);
|
|
668
693
|
}
|
|
669
694
|
}
|
|
670
|
-
catch {
|
|
671
|
-
throw
|
|
695
|
+
catch (cause) {
|
|
696
|
+
throw windowsCredentialStoreError('Windows parent directory chain is not trusted', cause);
|
|
672
697
|
}
|
|
673
698
|
}
|
|
674
699
|
isPosix() {
|
|
675
700
|
return this.platform !== 'win32';
|
|
676
701
|
}
|
|
677
702
|
}
|
|
703
|
+
function windowsCredentialStoreError(message, cause) {
|
|
704
|
+
const detail = cause instanceof Error ? windowsAclFailureDetail(cause) : '';
|
|
705
|
+
return new CredentialStoreError(detail ? `${message} (${detail})` : message, { cause });
|
|
706
|
+
}
|
|
678
707
|
const WINDOWS_ACL_SCRIPT = String.raw `
|
|
679
708
|
$ErrorActionPreference = 'Stop'
|
|
709
|
+
# Progress records can be serialized as CLIXML onto redirected stderr and
|
|
710
|
+
# obscure the actual failure. Suppress them so stderr carries only real errors.
|
|
711
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
712
|
+
|
|
713
|
+
function ConvertTo-OneLineAclDiagnostic([object]$Value) {
|
|
714
|
+
if ($null -eq $Value) { return '' }
|
|
715
|
+
return ([string]$Value -replace '[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+', ' ').Trim()
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function Throw-CredentialAclFailure(
|
|
719
|
+
[string]$Reason,
|
|
720
|
+
[string]$Path = '',
|
|
721
|
+
[string]$Sid = '',
|
|
722
|
+
[object]$Rights = $null,
|
|
723
|
+
[string]$Principal = ''
|
|
724
|
+
) {
|
|
725
|
+
$parts = @($Reason)
|
|
726
|
+
$safePath = ConvertTo-OneLineAclDiagnostic $Path
|
|
727
|
+
$safeSid = ConvertTo-OneLineAclDiagnostic $Sid
|
|
728
|
+
$safeRights = ConvertTo-OneLineAclDiagnostic $Rights
|
|
729
|
+
$safePrincipal = ConvertTo-OneLineAclDiagnostic $Principal
|
|
730
|
+
if (-not [string]::IsNullOrWhiteSpace($safePath)) { $parts += ('path=' + $safePath) }
|
|
731
|
+
if (-not [string]::IsNullOrWhiteSpace($safeSid)) { $parts += ('sid=' + $safeSid) }
|
|
732
|
+
if (-not [string]::IsNullOrWhiteSpace($safePrincipal)) { $parts += ('principal=' + $safePrincipal) }
|
|
733
|
+
if (-not [string]::IsNullOrWhiteSpace($safeRights)) { $parts += ('rights=' + $safeRights) }
|
|
734
|
+
throw ($parts -join '; ')
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
# Windows PowerShell can serialize ordinary error-stream writes as CLIXML when
|
|
738
|
+
# stderr is redirected. Write the terminating exception directly to native stderr so
|
|
739
|
+
# Node receives the actionable message rather than only a serialized record.
|
|
740
|
+
trap {
|
|
741
|
+
$message = ConvertTo-OneLineAclDiagnostic $_.Exception.Message
|
|
742
|
+
if ([string]::IsNullOrWhiteSpace($message)) { $message = 'Credential ACL check failed' }
|
|
743
|
+
[Console]::Error.WriteLine($message)
|
|
744
|
+
exit 1
|
|
745
|
+
}
|
|
746
|
+
|
|
680
747
|
$Target = [Environment]::GetEnvironmentVariable('EVOMAP_CREDENTIAL_ACL_TARGET', 'Process')
|
|
681
748
|
$Kind = [Environment]::GetEnvironmentVariable('EVOMAP_CREDENTIAL_ACL_KIND', 'Process')
|
|
682
749
|
if ([string]::IsNullOrEmpty($Target) -or
|
|
@@ -711,10 +778,10 @@ function Assert-TrustedParent([string]$ParentPath, [bool]$StrictCreate) {
|
|
|
711
778
|
$full = [System.IO.Path]::GetFullPath($ParentPath)
|
|
712
779
|
$root = [System.IO.Path]::GetPathRoot($full)
|
|
713
780
|
if ([string]::IsNullOrEmpty($root) -or $root -notmatch '^[A-Za-z]:\\$') {
|
|
714
|
-
|
|
781
|
+
Throw-CredentialAclFailure -Reason 'Credential parent must be on a local drive' -Path $full
|
|
715
782
|
}
|
|
716
783
|
if ([System.IO.DriveInfo]::new($root).DriveType -ne [System.IO.DriveType]::Fixed) {
|
|
717
|
-
|
|
784
|
+
Throw-CredentialAclFailure -Reason 'Credential parent must be on a fixed local drive' -Path $full
|
|
718
785
|
}
|
|
719
786
|
$current = $root
|
|
720
787
|
$relative = $full.Substring($root.Length)
|
|
@@ -741,12 +808,12 @@ function Assert-TrustedParent([string]$ParentPath, [bool]$StrictCreate) {
|
|
|
741
808
|
$item = Get-Item -LiteralPath $current -Force
|
|
742
809
|
if (-not $item.PSIsContainer -or
|
|
743
810
|
(($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
744
|
-
|
|
811
|
+
Throw-CredentialAclFailure -Reason 'Credential parent contains a reparse point or non-directory' -Path $current
|
|
745
812
|
}
|
|
746
813
|
$parentAcl = Get-Acl -LiteralPath $current
|
|
747
814
|
$ownerSid = $parentAcl.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
748
815
|
if ($trustedSids -notcontains $ownerSid.Value) {
|
|
749
|
-
|
|
816
|
+
Throw-CredentialAclFailure -Reason 'Credential parent has an untrusted owner' -Path $current -Sid $ownerSid.Value
|
|
750
817
|
}
|
|
751
818
|
foreach ($parentRule in @($parentAcl.Access)) {
|
|
752
819
|
if ($parentRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
|
|
@@ -783,10 +850,10 @@ function Assert-TrustedParent([string]$ParentPath, [bool]$StrictCreate) {
|
|
|
783
850
|
[System.Security.Principal.SecurityIdentifier]
|
|
784
851
|
)
|
|
785
852
|
} catch {
|
|
786
|
-
|
|
853
|
+
Throw-CredentialAclFailure -Reason 'Credential parent contains an unresolvable write principal' -Path $current -Principal $parentRule.IdentityReference.Value -Rights $rights
|
|
787
854
|
}
|
|
788
855
|
if ($trustedSids -notcontains $parentRuleSid.Value) {
|
|
789
|
-
|
|
856
|
+
Throw-CredentialAclFailure -Reason 'Credential parent grants write access to an untrusted principal' -Path $current -Sid $parentRuleSid.Value -Rights $rights
|
|
790
857
|
}
|
|
791
858
|
}
|
|
792
859
|
}
|
|
@@ -796,12 +863,12 @@ function Assert-TrustedFile([string]$FilePath) {
|
|
|
796
863
|
$item = Get-Item -LiteralPath $FilePath -Force
|
|
797
864
|
if ($item.PSIsContainer -or
|
|
798
865
|
(($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
799
|
-
|
|
866
|
+
Throw-CredentialAclFailure -Reason 'Credential file is a reparse point or not a regular file' -Path $FilePath
|
|
800
867
|
}
|
|
801
868
|
$fileAcl = Get-Acl -LiteralPath $FilePath
|
|
802
869
|
$ownerSid = $fileAcl.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
803
870
|
if ($trustedSids -notcontains $ownerSid.Value) {
|
|
804
|
-
|
|
871
|
+
Throw-CredentialAclFailure -Reason 'Credential file has an untrusted owner' -Path $FilePath -Sid $ownerSid.Value
|
|
805
872
|
}
|
|
806
873
|
foreach ($fileRule in @($fileAcl.Access)) {
|
|
807
874
|
if ($fileRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
|
|
@@ -822,10 +889,10 @@ function Assert-TrustedFile([string]$FilePath) {
|
|
|
822
889
|
[System.Security.Principal.SecurityIdentifier]
|
|
823
890
|
)
|
|
824
891
|
} catch {
|
|
825
|
-
|
|
892
|
+
Throw-CredentialAclFailure -Reason 'Credential file contains an unresolvable write principal' -Path $FilePath -Principal $fileRule.IdentityReference.Value -Rights $rights
|
|
826
893
|
}
|
|
827
894
|
if ($trustedSids -notcontains $fileRuleSid.Value) {
|
|
828
|
-
|
|
895
|
+
Throw-CredentialAclFailure -Reason 'Credential file grants write access to an untrusted principal' -Path $FilePath -Sid $fileRuleSid.Value -Rights $rights
|
|
829
896
|
}
|
|
830
897
|
}
|
|
831
898
|
}
|
|
@@ -896,7 +963,7 @@ $access = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
|
896
963
|
Set-Acl -LiteralPath $Target -AclObject $acl
|
|
897
964
|
$verified = Get-Acl -LiteralPath $Target
|
|
898
965
|
if (-not (Test-CanonicalCredentialAcl $verified $sid $expectedInheritance)) {
|
|
899
|
-
|
|
966
|
+
Throw-CredentialAclFailure -Reason 'Credential ACL verification failed' -Path $Target -Sid $sid.Value
|
|
900
967
|
}
|
|
901
968
|
`;
|
|
902
969
|
class PowerShellWindowsAclOps {
|
|
@@ -923,23 +990,37 @@ class PowerShellWindowsAclOps {
|
|
|
923
990
|
this.run(path, 'assert-file');
|
|
924
991
|
}
|
|
925
992
|
run(path, kind) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
993
|
+
try {
|
|
994
|
+
execFileSync(this.executable, [
|
|
995
|
+
'-NoLogo',
|
|
996
|
+
'-NoProfile',
|
|
997
|
+
'-NonInteractive',
|
|
998
|
+
'-ExecutionPolicy', 'Bypass',
|
|
999
|
+
// The fixed wrapper parses stdin once. Bare -Command - can execute
|
|
1000
|
+
// PowerShell 5.1 input statement by statement and mask an earlier error.
|
|
1001
|
+
'-Command', POWERSHELL_STDIN_SCRIPT_COMMAND,
|
|
1002
|
+
], {
|
|
1003
|
+
encoding: 'utf8',
|
|
1004
|
+
env: {
|
|
1005
|
+
SystemRoot: this.systemRoot,
|
|
1006
|
+
EVOMAP_CREDENTIAL_ACL_TARGET: path,
|
|
1007
|
+
EVOMAP_CREDENTIAL_ACL_KIND: kind,
|
|
1008
|
+
},
|
|
1009
|
+
shell: false,
|
|
1010
|
+
// Capture both streams rather than discarding them: the script's own
|
|
1011
|
+
// message names which path level and which SID failed, and without it
|
|
1012
|
+
// every rejection is indistinguishable from "PowerShell is missing".
|
|
1013
|
+
// The script prints nothing on success, so this stays quiet normally.
|
|
1014
|
+
input: WINDOWS_ACL_SCRIPT,
|
|
1015
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1016
|
+
timeout: 15_000,
|
|
1017
|
+
windowsHide: true,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
catch (cause) {
|
|
1021
|
+
const detail = windowsAclFailureDetail(cause);
|
|
1022
|
+
throw new Error(detail ? `${kind} check failed: ${detail}` : `${kind} check failed`, { cause });
|
|
1023
|
+
}
|
|
943
1024
|
}
|
|
944
1025
|
}
|
|
945
1026
|
function readDarwinAcl(path) {
|
|
@@ -999,6 +1080,13 @@ function sameIdentity(left, right) {
|
|
|
999
1080
|
function sameSecurityState(left, right) {
|
|
1000
1081
|
return sameIdentity(left, right) && left.ctimeNs === right.ctimeNs;
|
|
1001
1082
|
}
|
|
1083
|
+
function sameDarwinAncestorMetadata(left, right) {
|
|
1084
|
+
return sameIdentity(left, right)
|
|
1085
|
+
&& right.isDirectory()
|
|
1086
|
+
&& !right.isSymbolicLink()
|
|
1087
|
+
&& left.uid === right.uid
|
|
1088
|
+
&& left.mode === right.mode;
|
|
1089
|
+
}
|
|
1002
1090
|
function samePathSecurityStates(left, right) {
|
|
1003
1091
|
return left.length === right.length && left.every((state, index) => {
|
|
1004
1092
|
const candidate = right[index];
|
|
@@ -13,8 +13,8 @@ export type NodeSecretVersionHandler = (nodeSecretVersion?: number) => void;
|
|
|
13
13
|
*/
|
|
14
14
|
export type NodeSecretDivergenceHandler = () => void;
|
|
15
15
|
/**
|
|
16
|
-
* node_secret
|
|
17
|
-
*
|
|
16
|
+
* LegacyAuthShim exposes node_secret as a transport-neutral credential field. HubFetch promotes it to
|
|
17
|
+
* Authorization: Bearer for GET and strict GEP envelope endpoints before egress.
|
|
18
18
|
*/
|
|
19
19
|
export declare class LegacyAuthShim implements hub.AuthProvider {
|
|
20
20
|
private nodeSecret;
|
package/dist/auth/legacyShim.js
CHANGED
|
@@ -13,8 +13,8 @@ export function parseNodeSecretVersion(value) {
|
|
|
13
13
|
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* node_secret
|
|
17
|
-
*
|
|
16
|
+
* LegacyAuthShim exposes node_secret as a transport-neutral credential field. HubFetch promotes it to
|
|
17
|
+
* Authorization: Bearer for GET and strict GEP envelope endpoints before egress.
|
|
18
18
|
*/
|
|
19
19
|
export class LegacyAuthShim {
|
|
20
20
|
nodeSecret;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const POWERSHELL_STDIN_SCRIPT_COMMAND = '& ([scriptblock]::Create([Console]::In.ReadToEnd()))';
|
|
2
|
+
export function windowsAclFailureDetail(cause) {
|
|
3
|
+
const error = cause;
|
|
4
|
+
const parts = [];
|
|
5
|
+
if (typeof error?.code === 'string' && error.code) {
|
|
6
|
+
parts.push('code ' + normalizePowerShellDiagnostic(error.code));
|
|
7
|
+
}
|
|
8
|
+
if (typeof error?.status === 'number')
|
|
9
|
+
parts.push('exit ' + error.status);
|
|
10
|
+
if (typeof error?.signal === 'string' && error.signal) {
|
|
11
|
+
parts.push('signal ' + normalizePowerShellDiagnostic(error.signal));
|
|
12
|
+
}
|
|
13
|
+
const streams = [error?.stderr, error?.stdout]
|
|
14
|
+
.map((stream) => (typeof stream === 'string'
|
|
15
|
+
? stream
|
|
16
|
+
: Buffer.isBuffer(stream) ? stream.toString('utf8') : ''))
|
|
17
|
+
.map(stripPowerShellClixml)
|
|
18
|
+
.filter((text) => text.length > 0);
|
|
19
|
+
if (streams.length > 0) {
|
|
20
|
+
parts.push(streams.join(' | '));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const message = typeof error?.message === 'string' ? error.message : '';
|
|
24
|
+
const detail = stripPowerShellClixml(message.replace(/-EncodedCommand\s+\S+/g, '-EncodedCommand <omitted>'));
|
|
25
|
+
if (detail)
|
|
26
|
+
parts.push(detail);
|
|
27
|
+
}
|
|
28
|
+
return boundPowerShellDiagnostic(parts.join('; '));
|
|
29
|
+
}
|
|
30
|
+
export function stripPowerShellClixml(text) {
|
|
31
|
+
if (/#<\s*CLIXML\b/i.test(text)) {
|
|
32
|
+
const errorRecords = [];
|
|
33
|
+
const serializedErrors = /<S\b(?=[^>]*\bS=\x22Error\x22)[^>]*>([\s\S]*?)<\/S>/gi;
|
|
34
|
+
for (const match of text.matchAll(serializedErrors)) {
|
|
35
|
+
const record = decodePowerShellClixmlText(match[1] ?? '');
|
|
36
|
+
const hasLocationBoilerplate = /(?:^|\s)At line:\d+ char:\d+/i.test(record);
|
|
37
|
+
const decoded = normalizePowerShellDiagnostic(stripPowerShellLocationBoilerplate(record));
|
|
38
|
+
if (decoded)
|
|
39
|
+
errorRecords.push(decoded);
|
|
40
|
+
if (hasLocationBoilerplate)
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
if (errorRecords.length > 0)
|
|
44
|
+
return [...new Set(errorRecords)].join(' | ');
|
|
45
|
+
}
|
|
46
|
+
return normalizePowerShellDiagnostic(stripPowerShellLocationBoilerplate(text)
|
|
47
|
+
.replace(/<Objs\b[\s\S]*?<\/Objs>/g, ' ')
|
|
48
|
+
.replace(/<Objs\b[^\n]*/g, ' ')
|
|
49
|
+
.replace(/#<\s*CLIXML\b/gi, ' '));
|
|
50
|
+
}
|
|
51
|
+
function stripPowerShellLocationBoilerplate(text) {
|
|
52
|
+
return text.replace(/(?:^|\s)At line:\d+ char:\d+[\s\S]*$/i, ' ');
|
|
53
|
+
}
|
|
54
|
+
function decodePowerShellClixmlText(text) {
|
|
55
|
+
return text
|
|
56
|
+
.replace(/_x([0-9a-f]{4})_/gi, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
|
|
57
|
+
.replace(/&#x([0-9a-f]+);/gi, (match, value) => decodeXmlCodePoint(match, value, 16))
|
|
58
|
+
.replace(/&#([0-9]+);/g, (match, value) => decodeXmlCodePoint(match, value, 10))
|
|
59
|
+
.replace(/</g, '<')
|
|
60
|
+
.replace(/>/g, '>')
|
|
61
|
+
.replace(/"/g, String.fromCharCode(34))
|
|
62
|
+
.replace(/'/g, String.fromCharCode(39))
|
|
63
|
+
.replace(/&/g, '&');
|
|
64
|
+
}
|
|
65
|
+
function decodeXmlCodePoint(match, value, radix) {
|
|
66
|
+
const codePoint = Number.parseInt(value, radix);
|
|
67
|
+
if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff ||
|
|
68
|
+
(codePoint >= 0xd800 && codePoint <= 0xdfff)) {
|
|
69
|
+
return match;
|
|
70
|
+
}
|
|
71
|
+
return String.fromCodePoint(codePoint);
|
|
72
|
+
}
|
|
73
|
+
const POWERSHELL_DIAGNOSTIC_FORMAT_CHARACTER = /^\p{Cf}$/u;
|
|
74
|
+
function normalizePowerShellDiagnostic(text) {
|
|
75
|
+
const sanitized = [];
|
|
76
|
+
for (const character of text) {
|
|
77
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
78
|
+
const isControl = codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
79
|
+
const isFormat = POWERSHELL_DIAGNOSTIC_FORMAT_CHARACTER.test(character);
|
|
80
|
+
sanitized.push(isControl || isFormat || codePoint === 0x2028 || codePoint === 0x2029 ? ' ' : character);
|
|
81
|
+
}
|
|
82
|
+
return sanitized.join('').replace(/\s+/g, ' ').trim();
|
|
83
|
+
}
|
|
84
|
+
function boundPowerShellDiagnostic(text) {
|
|
85
|
+
const limit = 300;
|
|
86
|
+
if (text.length <= limit)
|
|
87
|
+
return text;
|
|
88
|
+
const headLength = Math.ceil((limit - 3) / 2);
|
|
89
|
+
const tailLength = limit - 3 - headLength;
|
|
90
|
+
return text.slice(0, headLength) + '...' + text.slice(-tailLength);
|
|
91
|
+
}
|
package/dist/hubCapability.d.ts
CHANGED
|
@@ -58,6 +58,9 @@ export interface AccountAssetListResult {
|
|
|
58
58
|
hasMore: boolean;
|
|
59
59
|
nextCursor?: string;
|
|
60
60
|
}
|
|
61
|
+
export declare class MalformedAccountAssetPageError extends Error {
|
|
62
|
+
constructor();
|
|
63
|
+
}
|
|
61
64
|
/** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
|
|
62
65
|
export declare function gepEnvelope(messageType: string, payload: unknown, options?: {
|
|
63
66
|
messageId?: string;
|
|
@@ -142,12 +145,20 @@ export declare class PublicHubCapability implements hub.HubCapability {
|
|
|
142
145
|
readonly auth: hub.AuthProvider;
|
|
143
146
|
readonly recipes: hub.RecipeCapability;
|
|
144
147
|
constructor(opts: PublicHubOptions);
|
|
148
|
+
private evolverVersionForWire;
|
|
149
|
+
private envFingerprintForWire;
|
|
145
150
|
hello(opts: PublicHelloOptions): Promise<PublicHelloResult>;
|
|
146
151
|
heartbeat(opts?: PublicHeartbeatOptions): Promise<PublicHeartbeatResult>;
|
|
147
152
|
private heartbeatMeta;
|
|
148
153
|
publish(bundle: hub.AssetRecord[], options?: hub.PublishOptions): Promise<hub.PublishReceipt>;
|
|
149
154
|
fetch(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
|
|
150
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
|
|
157
|
+
* `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
|
|
158
|
+
* client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
|
|
159
|
+
*/
|
|
160
|
+
fetchAssetDeliveryById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetDeliveryOutcome>;
|
|
161
|
+
fetchAssetById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetRecord | null>;
|
|
151
162
|
/**
|
|
152
163
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
153
164
|
* signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
|
|
@@ -183,6 +194,8 @@ export declare class PublicHubCapability implements hub.HubCapability {
|
|
|
183
194
|
createRecipe(request: hub.RecipeCreateRequest): Promise<hub.RecipeReceipt>;
|
|
184
195
|
publishRecipe(recipeId: string, options?: hub.RecipePublishOptions): Promise<hub.RecipeReceipt>;
|
|
185
196
|
getRecipe(recipeId: string): Promise<hub.RecipeFetchReceipt>;
|
|
197
|
+
searchRecipes(request?: hub.RecipeSearchRequest): Promise<hub.RecipeSearchReceipt>;
|
|
198
|
+
listRecipes(request?: hub.RecipeSearchRequest): Promise<hub.RecipeSearchReceipt>;
|
|
186
199
|
expressRecipe(recipeId: string, request?: hub.RecipeExpressRequest): Promise<hub.RecipeExpressionReceipt>;
|
|
187
200
|
task: {
|
|
188
201
|
claim: (taskId: string) => Promise<{
|