@brainai/satp-client 2.0.2 → 2.0.4

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/src/index.js CHANGED
@@ -787,10 +787,16 @@ const {
787
787
  DECISIONS,
788
788
  DEFAULT_POLICY,
789
789
  REASON_CODES,
790
+ RUNTIME_POLICY_AUDIT_TRACE_SCHEMA_VERSION,
791
+ RUNTIME_POLICY_HOST_ACTION_DESCRIPTOR_SCHEMA_VERSION,
792
+ buildRuntimePolicyActionDescriptor,
793
+ buildRuntimePolicyAuditTrace,
794
+ createRuntimePolicyAdapter,
790
795
  evaluateRuntimePolicy,
791
796
  } = require('./runtime-policy-adapter');
792
797
  const walletControlChallenge = require('./wallet-control-challenge');
793
798
  const x402Discovery = require('./x402-discovery');
799
+ const signerPolicy = require('./signer-policy');
794
800
 
795
801
  // Legacy V3 SDK wrapper — keeps string constructor compatibility while using
796
802
  // the local extracted SATPV3SDK implementation so offline tests and consumers
@@ -905,7 +911,12 @@ module.exports = {
905
911
 
906
912
  // V3 PDA derivation (local extracted scaffold)
907
913
  PROGRAM_IDS: v3pda.getV3ProgramIds('devnet'),
914
+ V3_DEVNET_PROGRAM_IDS: v3pda.V3_DEVNET_PROGRAM_IDS,
915
+ V3_MAINNET_PROGRAM_IDS: v3pda.V3_MAINNET_PROGRAM_IDS,
908
916
  getV3ProgramIds: v3pda.getV3ProgramIds,
917
+ V3_DEVNET_TOKEN_MINTS: v3pda.V3_DEVNET_TOKEN_MINTS,
918
+ SPL_TOKEN_PROGRAM_ID: v3pda.SPL_TOKEN_PROGRAM_ID,
919
+ ASSOCIATED_TOKEN_PROGRAM_ID: v3pda.ASSOCIATED_TOKEN_PROGRAM_ID,
909
920
  hashAgentId: v3pda.hashAgentId,
910
921
  agentIdHash: v3pda.hashAgentId,
911
922
  hashName: v3pda.hashName,
@@ -929,6 +940,8 @@ module.exports = {
929
940
  deriveAttestationPda: v3pda.getV3AttestationPDA,
930
941
  getV3EscrowPDA: v3pda.getV3EscrowPDA,
931
942
  deriveEscrowPda: v3pda.getV3EscrowPDA,
943
+ getAssociatedTokenAddress: v3pda.getAssociatedTokenAddress,
944
+ getV3EscrowVaultATA: v3pda.getV3EscrowVaultATA,
932
945
  deriveReviewAttestationPda: getReviewAttestationPDA,
933
946
  prepareIdentityAttestationRequest,
934
947
  TRUST_PACKET_SCHEMA_VERSION,
@@ -950,11 +963,21 @@ module.exports = {
950
963
  buildX402EvidenceLookup: x402Discovery.buildX402EvidenceLookup,
951
964
  buildRuntimePolicyActionDescriptorFromX402Discovery: x402Discovery.buildRuntimePolicyActionDescriptorFromX402Discovery,
952
965
  buildRuntimePolicyActionDescriptorFromX402: x402Discovery.buildRuntimePolicyActionDescriptorFromX402,
966
+ SATP_SIGNER_ROLES: signerPolicy.SATP_SIGNER_ROLES,
967
+ OPERATIONAL_SIGNER_ALLOWED_ACTIONS: signerPolicy.OPERATIONAL_SIGNER_ALLOWED_ACTIONS,
968
+ OWNER_UPGRADE_AUTHORITY_BLOCKED_ACTIONS: signerPolicy.OWNER_UPGRADE_AUTHORITY_BLOCKED_ACTIONS,
969
+ buildSignerSeparationConfig: signerPolicy.buildSignerSeparationConfig,
970
+ validateSignerSeparationConfig: signerPolicy.validateSignerSeparationConfig,
953
971
 
954
972
  // Runtime policy adapter (offline/local guardrail helper)
955
973
  DECISIONS,
956
974
  DEFAULT_POLICY,
957
975
  REASON_CODES,
976
+ RUNTIME_POLICY_AUDIT_TRACE_SCHEMA_VERSION,
977
+ RUNTIME_POLICY_HOST_ACTION_DESCRIPTOR_SCHEMA_VERSION,
978
+ buildRuntimePolicyActionDescriptor,
979
+ buildRuntimePolicyAuditTrace,
980
+ createRuntimePolicyAdapter,
958
981
  evaluateRuntimePolicy,
959
982
 
960
983
  // V3 Deserialization (local extracted scaffold)
@@ -34,6 +34,62 @@ const DEFAULT_POLICY = Object.freeze({
34
34
  staleEvidenceAfterMs: 7 * 24 * 60 * 60 * 1000,
35
35
  });
36
36
 
37
+ const RUNTIME_POLICY_AUDIT_TRACE_SCHEMA_VERSION = 'satp.runtimePolicyAuditTrace.v1';
38
+ const RUNTIME_POLICY_HOST_ACTION_DESCRIPTOR_SCHEMA_VERSION = 'satp.runtimePolicyHostActionDescriptor.v1';
39
+
40
+ function createRuntimePolicyAdapter(config = {}) {
41
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
42
+ throw new Error('runtime policy adapter config must be an object');
43
+ }
44
+
45
+ const adapterPolicy = normalizePolicy(config.policy);
46
+ const defaultActionType = config.defaultActionType || null;
47
+ const nowProvider = config.now;
48
+ const redact = config.redact;
49
+
50
+ if (nowProvider !== undefined && typeof nowProvider !== 'function' && !isValidDateInput(nowProvider)) {
51
+ throw new Error('runtime policy adapter now must be a function or valid date input');
52
+ }
53
+ if (redact !== undefined && typeof redact !== 'function') {
54
+ throw new Error('runtime policy adapter redact must be a function');
55
+ }
56
+
57
+ function adapterOptions(options = {}) {
58
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
59
+ throw new Error('runtime policy adapter method options must be an object');
60
+ }
61
+
62
+ return {
63
+ ...options,
64
+ now: resolveAdapterNow(nowProvider, options.now),
65
+ policy: {
66
+ ...adapterPolicy,
67
+ ...(options.policy || {}),
68
+ },
69
+ };
70
+ }
71
+
72
+ return Object.freeze({
73
+ action(input = {}, overrides = {}) {
74
+ const base = applyDefaultActionType(input, defaultActionType);
75
+ return buildRuntimePolicyActionDescriptor(base, overrides);
76
+ },
77
+
78
+ evaluate(identityPayload, actionDescriptor, options = {}) {
79
+ return evaluateRuntimePolicy(identityPayload, actionDescriptor, adapterOptions(options));
80
+ },
81
+
82
+ auditTrace(identityPayload, actionDescriptor, options = {}) {
83
+ const traceAction = redact ? attachRedactedResourceLabel(actionDescriptor, redact) : actionDescriptor;
84
+ return buildRuntimePolicyAuditTrace(identityPayload, traceAction, adapterOptions(options));
85
+ },
86
+
87
+ explain(result) {
88
+ return explainRuntimePolicyResult(result);
89
+ },
90
+ });
91
+ }
92
+
37
93
  function evaluateRuntimePolicy(identityPayload, actionDescriptor, options = {}) {
38
94
  const policy = { ...DEFAULT_POLICY, ...(options.policy || {}) };
39
95
  const now = options.now ? new Date(options.now) : new Date();
@@ -109,6 +165,94 @@ function evaluateRuntimePolicy(identityPayload, actionDescriptor, options = {})
109
165
  return decision(DECISIONS.ALLOW, reasonCodes, checks, 'Local runtime policy allows the action.');
110
166
  }
111
167
 
168
+ function buildRuntimePolicyAuditTrace(identityPayload, actionDescriptor, options = {}) {
169
+ const result = options.result || evaluateRuntimePolicy(identityPayload, actionDescriptor, options);
170
+ const identity = normalizeIdentity(identityPayload);
171
+ const action = normalizeAction(actionDescriptor);
172
+
173
+ return {
174
+ schemaVersion: RUNTIME_POLICY_AUDIT_TRACE_SCHEMA_VERSION,
175
+ mode: 'offline-local-runtime-policy-trace',
176
+ generatedAt: safeIsoDate(options.now),
177
+ decision: result.decision,
178
+ reasonCodes: result.reasonCodes.slice(),
179
+ message: result.message,
180
+ subject: {
181
+ agentId: identity.agentId,
182
+ active: identity.active,
183
+ verified: identity.verified,
184
+ trustScoreBand: trustScoreBand(identity.trustScore),
185
+ evidenceUpdatedAt: identity.evidenceUpdatedAt,
186
+ capabilityCount: identity.capabilities.length,
187
+ },
188
+ action: {
189
+ type: action.type,
190
+ operation: action.operation,
191
+ resourceKind: resourceKind(action.resource),
192
+ resourceLabel: action.resourceLabel,
193
+ requiresCapability: action.requiresCapability,
194
+ requiresFreshEvidence: action.requiresFreshEvidence,
195
+ protectedTool: action.protectedTool,
196
+ operatorApprovalRequired: action.operatorApprovalRequired,
197
+ costUsd: action.costUsd,
198
+ evidenceLookup: action.evidenceLookup
199
+ ? {
200
+ type: action.evidenceLookup.type || null,
201
+ configured: true,
202
+ maxCostUsd: action.evidenceLookup.maxCostUsd ?? null,
203
+ }
204
+ : null,
205
+ },
206
+ checks: sanitizeAuditChecks(result.checks),
207
+ guardrails: {
208
+ localDecisionOnly: true,
209
+ writesSolanaState: false,
210
+ usesKeypairs: false,
211
+ deploysPrograms: false,
212
+ publishesPackages: false,
213
+ authorizesPayment: false,
214
+ authorizesAgentActionFromPayment: false,
215
+ },
216
+ };
217
+ }
218
+
219
+ function buildRuntimePolicyActionDescriptor(input = {}, overrides = {}) {
220
+ const base = typeof input === 'string' ? { type: input } : input;
221
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
222
+ throw new Error('runtime policy action descriptor input must be an object or action type string');
223
+ }
224
+ if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides)) {
225
+ throw new Error('runtime policy action descriptor overrides must be an object');
226
+ }
227
+
228
+ const action = { ...base, ...overrides };
229
+ const type = action.type || action.surface || 'generic';
230
+ const descriptor = {
231
+ schemaVersion: RUNTIME_POLICY_HOST_ACTION_DESCRIPTOR_SCHEMA_VERSION,
232
+ type,
233
+ resource: firstDefined(action.resource, defaultResourceForAction(type, action)),
234
+ operation: firstDefined(action.operation, defaultOperationForAction(type)),
235
+ requiresCapability: firstDefined(action.requiresCapability, action.capability, defaultCapabilityForAction(type)),
236
+ minimumTrustScore: firstDefined(action.minimumTrustScore, action.trustScoreMinimum, defaultMinimumTrustScoreForAction(type)),
237
+ allowDegraded: firstDefined(action.allowDegraded, defaultAllowDegradedForAction(type)),
238
+ requiresFreshEvidence: firstDefined(action.requiresFreshEvidence, defaultRequiresFreshEvidenceForAction(type)),
239
+ costUsd: firstDefined(action.costUsd, defaultCostUsdForAction(type)),
240
+ protectedTool: firstDefined(action.protectedTool, type === 'mcp_protected_tool'),
241
+ operatorApprovalRequired: firstDefined(action.operatorApprovalRequired, false),
242
+ guardrails: {
243
+ localDecisionOnly: true,
244
+ writesSolanaState: false,
245
+ usesKeypairs: false,
246
+ deploysPrograms: false,
247
+ publishesPackages: false,
248
+ livePaymentRequired: false,
249
+ },
250
+ };
251
+
252
+ if (action.evidenceLookup !== undefined) descriptor.evidenceLookup = action.evidenceLookup;
253
+ return descriptor;
254
+ }
255
+
112
256
  function staleEvidenceDecision(action, options, reasonCodes, checks) {
113
257
  const lookup = action.evidenceLookup || null;
114
258
  if (!lookup || lookup.type !== 'x402') {
@@ -158,6 +302,7 @@ function normalizeAction(action = {}) {
158
302
  return {
159
303
  type: action.type || 'generic',
160
304
  resource: action.resource || null,
305
+ resourceLabel: action.resourceLabel || null,
161
306
  operation: action.operation || null,
162
307
  requiresCapability: action.requiresCapability || null,
163
308
  minimumTrustScore: Number.isFinite(action.minimumTrustScore) ? action.minimumTrustScore : null,
@@ -189,6 +334,149 @@ function clampScore(value) {
189
334
  return Math.max(0, Math.min(100, score));
190
335
  }
191
336
 
337
+ function trustScoreBand(score) {
338
+ if (score >= 90) return '90-100';
339
+ if (score >= 80) return '80-89';
340
+ if (score >= 70) return '70-79';
341
+ if (score >= 50) return '50-69';
342
+ if (score >= 25) return '25-49';
343
+ return '0-24';
344
+ }
345
+
346
+ function sanitizeAuditChecks(checks = {}) {
347
+ const sanitized = { ...checks };
348
+ if (sanitized.evidenceLookup && typeof sanitized.evidenceLookup === 'object') {
349
+ sanitized.evidenceLookup = {
350
+ type: sanitized.evidenceLookup.type || null,
351
+ configured: true,
352
+ maxCostUsd: sanitized.evidenceLookup.maxCostUsd ?? null,
353
+ };
354
+ }
355
+ return sanitized;
356
+ }
357
+
358
+ function firstDefined(...values) {
359
+ return values.find((value) => value !== undefined);
360
+ }
361
+
362
+ function defaultResourceForAction(type, action) {
363
+ if (type === 'mcp_protected_tool') return 'mcp://protected/tool';
364
+ if (type === 'agentfolio_trust_gate') {
365
+ if (action.profileId) {
366
+ return `https://agentfolio.bot/api/profile/${encodeURIComponent(String(action.profileId))}/trust-score`;
367
+ }
368
+ return 'https://agentfolio.bot/api/profile/:id/trust-score';
369
+ }
370
+ if (type === 'x402_endpoint') return 'x402://paid-endpoint';
371
+ return null;
372
+ }
373
+
374
+ function defaultOperationForAction(type) {
375
+ if (type === 'mcp_protected_tool') return 'invoke';
376
+ if (type === 'agentfolio_trust_gate') return 'trust-score-read';
377
+ if (type === 'x402_endpoint') return 'lookup';
378
+ return null;
379
+ }
380
+
381
+ function defaultCapabilityForAction(type) {
382
+ if (type === 'agentfolio_trust_gate') return 'agentfolio:trust-read';
383
+ return null;
384
+ }
385
+
386
+ function defaultMinimumTrustScoreForAction(type) {
387
+ if (type === 'agentfolio_trust_gate') return DEFAULT_POLICY.minimumTrustScore;
388
+ return null;
389
+ }
390
+
391
+ function defaultAllowDegradedForAction(type) {
392
+ return type === 'agentfolio_trust_gate';
393
+ }
394
+
395
+ function defaultRequiresFreshEvidenceForAction(type) {
396
+ return type === 'mcp_protected_tool' || type === 'agentfolio_trust_gate';
397
+ }
398
+
399
+ function defaultCostUsdForAction(type) {
400
+ if (type === 'x402_endpoint') return 0;
401
+ return 0;
402
+ }
403
+
404
+ function normalizePolicy(policy) {
405
+ if (policy === undefined) return {};
406
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
407
+ throw new Error('runtime policy adapter policy must be an object');
408
+ }
409
+ return { ...policy };
410
+ }
411
+
412
+ function applyDefaultActionType(input, defaultActionType) {
413
+ if (!defaultActionType || typeof input === 'string') return input;
414
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
415
+ if (input.type || input.surface) return input;
416
+ return { type: defaultActionType, ...input };
417
+ }
418
+
419
+ function resolveAdapterNow(nowProvider, methodNow) {
420
+ if (methodNow !== undefined) return methodNow;
421
+ if (typeof nowProvider === 'function') return nowProvider();
422
+ return nowProvider;
423
+ }
424
+
425
+ function isValidDateInput(value) {
426
+ const date = new Date(value);
427
+ return !Number.isNaN(date.getTime());
428
+ }
429
+
430
+ function attachRedactedResourceLabel(actionDescriptor, redact) {
431
+ if (!actionDescriptor || typeof actionDescriptor !== 'object' || Array.isArray(actionDescriptor)) {
432
+ return actionDescriptor;
433
+ }
434
+ if (typeof actionDescriptor.resource !== 'string') return actionDescriptor;
435
+ const redacted = redact(actionDescriptor.resource);
436
+ return {
437
+ ...actionDescriptor,
438
+ resourceLabel: typeof redacted === 'string' ? redacted : '[redacted]',
439
+ };
440
+ }
441
+
442
+ function explainRuntimePolicyResult(result = {}) {
443
+ const reasonCodes = Array.isArray(result.reasonCodes) ? result.reasonCodes : [];
444
+ return reasonCodes.map((code) => RUNTIME_POLICY_EXPLANATIONS[code] || `Runtime policy returned ${code}.`);
445
+ }
446
+
447
+ const RUNTIME_POLICY_EXPLANATIONS = Object.freeze({
448
+ [REASON_CODES.ACTION_PAYMENT_NEEDS_APPROVAL]: 'The action cost exceeds the host auto-spend policy and needs approval.',
449
+ [REASON_CODES.ACTION_PAYMENT_PREAPPROVED]: 'The host preapproved payment for this action.',
450
+ [REASON_CODES.EVIDENCE_FRESH]: 'The identity evidence is fresh enough for this action.',
451
+ [REASON_CODES.EVIDENCE_STALE_OR_MISSING]: 'The identity evidence is stale or missing.',
452
+ [REASON_CODES.IDENTITY_INACTIVE]: 'The identity is inactive.',
453
+ [REASON_CODES.IDENTITY_UNVERIFIED]: 'The identity is not verified by the host policy.',
454
+ [REASON_CODES.INVALID_ACTION_COST_USD]: 'The action cost must be a finite non-negative number.',
455
+ [REASON_CODES.LOCAL_POLICY_ALLOW]: 'The local host policy allows the action.',
456
+ [REASON_CODES.MISSING_CAPABILITY]: 'The identity is missing the capability required by this action.',
457
+ [REASON_CODES.PROTECTED_TOOL_REQUIRES_APPROVAL]: 'The protected tool requires operator approval.',
458
+ [REASON_CODES.TRUST_SCORE_BELOW_DENY_FLOOR]: 'The trust score is below the host deny floor.',
459
+ [REASON_CODES.TRUST_SCORE_BELOW_MINIMUM]: 'The trust score is below the minimum for this action.',
460
+ [REASON_CODES.TRUST_SCORE_OK]: 'The trust score satisfies the local policy.',
461
+ [REASON_CODES.X402_LOOKUP_PAYMENT_PREAPPROVED]: 'The host preapproved payment for the x402 evidence lookup.',
462
+ [REASON_CODES.X402_LOOKUP_REQUIRES_APPROVAL]: 'The x402 evidence lookup requires payment approval.',
463
+ [REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION]: 'x402 payment does not authorize the agent action.',
464
+ });
465
+
466
+ function safeIsoDate(value) {
467
+ const date = value ? new Date(value) : new Date();
468
+ if (Number.isNaN(date.getTime())) return new Date().toISOString();
469
+ return date.toISOString();
470
+ }
471
+
472
+ function resourceKind(resource) {
473
+ if (!resource || typeof resource !== 'string') return null;
474
+ const schemeMatch = resource.match(/^([a-z][a-z0-9+.-]*):/i);
475
+ if (schemeMatch) return `${schemeMatch[1].toLowerCase()}:`;
476
+ if (resource.startsWith('/')) return 'path';
477
+ return 'opaque';
478
+ }
479
+
192
480
  function isEvidenceFresh(identity, policy, now) {
193
481
  if (!identity.evidenceUpdatedAt) return false;
194
482
  const updatedAt = new Date(identity.evidenceUpdatedAt);
@@ -202,5 +490,10 @@ module.exports = {
202
490
  DECISIONS,
203
491
  DEFAULT_POLICY,
204
492
  REASON_CODES,
493
+ RUNTIME_POLICY_AUDIT_TRACE_SCHEMA_VERSION,
494
+ RUNTIME_POLICY_HOST_ACTION_DESCRIPTOR_SCHEMA_VERSION,
495
+ buildRuntimePolicyActionDescriptor,
496
+ buildRuntimePolicyAuditTrace,
497
+ createRuntimePolicyAdapter,
205
498
  evaluateRuntimePolicy,
206
499
  };
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const { PublicKey } = require('@solana/web3.js');
4
+
5
+ const SATP_SIGNER_ROLES = Object.freeze({
6
+ OPERATIONAL_SIGNER: 'operational_signer',
7
+ OWNER_UPGRADE_AUTHORITY: 'owner_upgrade_authority',
8
+ });
9
+
10
+ const OPERATIONAL_SIGNER_ALLOWED_ACTIONS = Object.freeze([
11
+ 'devnet_fee_payment',
12
+ 'devnet_transaction_submission',
13
+ 'offline_transaction_preparation',
14
+ 'read_only_rpc',
15
+ ]);
16
+
17
+ const OWNER_UPGRADE_AUTHORITY_BLOCKED_ACTIONS = Object.freeze([
18
+ 'program_upgrade',
19
+ 'authority_transfer',
20
+ 'key_generation',
21
+ 'key_rotation',
22
+ 'mainnet_deploy',
23
+ 'devnet_deploy',
24
+ 'npm_publish',
25
+ 'funds_custody',
26
+ 'funds_transfer',
27
+ ]);
28
+
29
+ const OPERATIONAL_SIGNER_AUTHORITY_BOUNDARY =
30
+ 'no_upgrade_authority_no_key_management_no_funds_custody';
31
+
32
+ const OWNER_UPGRADE_AUTHORITY_CUSTODY = 'owner_held';
33
+
34
+ const SECRET_FIELD_NAMES = Object.freeze([
35
+ 'keypair',
36
+ 'keypairPath',
37
+ 'secretKey',
38
+ 'privateKey',
39
+ 'seedPhrase',
40
+ 'mnemonic',
41
+ 'rawEnv',
42
+ ]);
43
+
44
+ function normalizePublicKey(value, fieldName) {
45
+ if (value === undefined || value === null || value === '') {
46
+ throw new Error(`${fieldName} is required`);
47
+ }
48
+ return new PublicKey(value).toBase58();
49
+ }
50
+
51
+ function assertNoSecretFields(input, path = 'config') {
52
+ if (!input || typeof input !== 'object') return;
53
+
54
+ for (const [key, value] of Object.entries(input)) {
55
+ const childPath = `${path}.${key}`;
56
+ if (SECRET_FIELD_NAMES.includes(key)) {
57
+ throw new Error(`${childPath} must not be present in SATP signer policy config`);
58
+ }
59
+ if (value && typeof value === 'object' && !Buffer.isBuffer(value) && !(value instanceof PublicKey)) {
60
+ assertNoSecretFields(value, childPath);
61
+ }
62
+ }
63
+ }
64
+
65
+ function normalizeOperationalActions(actions) {
66
+ const requested = actions === undefined ? OPERATIONAL_SIGNER_ALLOWED_ACTIONS : actions;
67
+ if (!Array.isArray(requested)) {
68
+ throw new Error('operational signer actions must be an array');
69
+ }
70
+
71
+ const seen = new Set();
72
+ for (const action of requested) {
73
+ if (typeof action !== 'string' || action.length === 0) {
74
+ throw new Error('operational signer actions must be non-empty strings');
75
+ }
76
+ if (!OPERATIONAL_SIGNER_ALLOWED_ACTIONS.includes(action)) {
77
+ throw new Error(`operational signer action is not low-privilege: ${action}`);
78
+ }
79
+ if (OWNER_UPGRADE_AUTHORITY_BLOCKED_ACTIONS.includes(action)) {
80
+ throw new Error(`operational signer action is owner-gated: ${action}`);
81
+ }
82
+ seen.add(action);
83
+ }
84
+
85
+ return Array.from(seen).sort();
86
+ }
87
+
88
+ function arraysMatchExactly(actual, expected) {
89
+ if (!Array.isArray(actual) || actual.length !== expected.length) {
90
+ return false;
91
+ }
92
+
93
+ const sortedActual = actual.slice().sort();
94
+ const sortedExpected = expected.slice().sort();
95
+ return sortedExpected.every((action, index) => sortedActual[index] === action);
96
+ }
97
+
98
+ function buildSignerSeparationConfig(opts = {}) {
99
+ assertNoSecretFields(opts);
100
+
101
+ const network = opts.network || 'devnet';
102
+ if (network !== 'devnet' && network !== 'mainnet') {
103
+ throw new Error('Invalid network: expected devnet or mainnet');
104
+ }
105
+
106
+ const operationalSigner = normalizePublicKey(
107
+ opts.operationalSignerPublicKey,
108
+ 'operationalSignerPublicKey',
109
+ );
110
+ const ownerUpgradeAuthority = normalizePublicKey(
111
+ opts.ownerUpgradeAuthorityPublicKey,
112
+ 'ownerUpgradeAuthorityPublicKey',
113
+ );
114
+
115
+ if (operationalSigner === ownerUpgradeAuthority) {
116
+ throw new Error('operational signer must be distinct from Owner upgrade authority');
117
+ }
118
+
119
+ return {
120
+ schemaVersion: 'satp.signerSeparation.v1',
121
+ network,
122
+ operationalSigner: {
123
+ role: SATP_SIGNER_ROLES.OPERATIONAL_SIGNER,
124
+ publicKey: operationalSigner,
125
+ allowedActions: normalizeOperationalActions(opts.operationalAllowedActions),
126
+ blockedActions: OWNER_UPGRADE_AUTHORITY_BLOCKED_ACTIONS.slice().sort(),
127
+ authorityBoundary: OPERATIONAL_SIGNER_AUTHORITY_BOUNDARY,
128
+ },
129
+ ownerUpgradeAuthority: {
130
+ role: SATP_SIGNER_ROLES.OWNER_UPGRADE_AUTHORITY,
131
+ publicKey: ownerUpgradeAuthority,
132
+ custody: OWNER_UPGRADE_AUTHORITY_CUSTODY,
133
+ operationalSignerMayUse: false,
134
+ },
135
+ flags: {
136
+ publicKeysOnly: true,
137
+ readsKeypairs: false,
138
+ generatesKeypairs: false,
139
+ transfersAuthority: false,
140
+ deploysPrograms: false,
141
+ publishesPackages: false,
142
+ writesSolanaState: false,
143
+ },
144
+ };
145
+ }
146
+
147
+ function validateSignerSeparationConfig(config) {
148
+ try {
149
+ assertNoSecretFields(config);
150
+ const normalized = buildSignerSeparationConfig({
151
+ network: config && config.network,
152
+ operationalSignerPublicKey: config && config.operationalSigner && config.operationalSigner.publicKey,
153
+ ownerUpgradeAuthorityPublicKey: config && config.ownerUpgradeAuthority && config.ownerUpgradeAuthority.publicKey,
154
+ operationalAllowedActions: config && config.operationalSigner && config.operationalSigner.allowedActions,
155
+ });
156
+
157
+ const errors = [];
158
+ if (!config || config.schemaVersion !== 'satp.signerSeparation.v1') {
159
+ errors.push('schemaVersion must be satp.signerSeparation.v1');
160
+ }
161
+ if (!config || !config.flags || config.flags.publicKeysOnly !== true) {
162
+ errors.push('flags.publicKeysOnly must be true');
163
+ }
164
+ if (!config || !config.operationalSigner || config.operationalSigner.role !== SATP_SIGNER_ROLES.OPERATIONAL_SIGNER) {
165
+ errors.push('operationalSigner.role must be operational_signer');
166
+ }
167
+ if (!config || !config.ownerUpgradeAuthority || config.ownerUpgradeAuthority.role !== SATP_SIGNER_ROLES.OWNER_UPGRADE_AUTHORITY) {
168
+ errors.push('ownerUpgradeAuthority.role must be owner_upgrade_authority');
169
+ }
170
+ if (!config || !config.operationalSigner || config.operationalSigner.authorityBoundary !== OPERATIONAL_SIGNER_AUTHORITY_BOUNDARY) {
171
+ errors.push(`operationalSigner.authorityBoundary must be ${OPERATIONAL_SIGNER_AUTHORITY_BOUNDARY}`);
172
+ }
173
+ if (!config || !config.ownerUpgradeAuthority || config.ownerUpgradeAuthority.custody !== OWNER_UPGRADE_AUTHORITY_CUSTODY) {
174
+ errors.push(`ownerUpgradeAuthority.custody must be ${OWNER_UPGRADE_AUTHORITY_CUSTODY}`);
175
+ }
176
+ if (
177
+ !config
178
+ || !config.operationalSigner
179
+ || !arraysMatchExactly(config.operationalSigner.blockedActions, OWNER_UPGRADE_AUTHORITY_BLOCKED_ACTIONS)
180
+ ) {
181
+ errors.push('operationalSigner.blockedActions must list all owner-gated actions');
182
+ }
183
+ if (config && config.ownerUpgradeAuthority && config.ownerUpgradeAuthority.operationalSignerMayUse !== false) {
184
+ errors.push('ownerUpgradeAuthority.operationalSignerMayUse must be false');
185
+ }
186
+ for (const flagName of [
187
+ 'readsKeypairs',
188
+ 'generatesKeypairs',
189
+ 'transfersAuthority',
190
+ 'deploysPrograms',
191
+ 'publishesPackages',
192
+ 'writesSolanaState',
193
+ ]) {
194
+ if (!config || !config.flags || config.flags[flagName] !== false) {
195
+ errors.push(`flags.${flagName} must be false`);
196
+ }
197
+ }
198
+ return { ok: errors.length === 0, errors, normalized };
199
+ } catch (e) {
200
+ return { ok: false, errors: [e.message], normalized: null };
201
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ SATP_SIGNER_ROLES,
206
+ OPERATIONAL_SIGNER_ALLOWED_ACTIONS,
207
+ OWNER_UPGRADE_AUTHORITY_BLOCKED_ACTIONS,
208
+ buildSignerSeparationConfig,
209
+ validateSignerSeparationConfig,
210
+ };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ // Generated by `node scripts/generate-v3-idls.mjs`; do not edit by hand.
4
+ const V3_IDL_ACCOUNT_DISCRIMINATORS = Object.freeze({
5
+ "Attestation": Object.freeze([152, 125, 183, 86, 36, 146, 121, 73]),
6
+ "EscrowV3": Object.freeze([145, 108, 37, 52, 197, 162, 232, 59]),
7
+ "GenesisRecord": Object.freeze([22, 160, 245, 112, 178, 126, 177, 106]),
8
+ "LinkedWallet": Object.freeze([43, 44, 217, 238, 93, 127, 166, 59]),
9
+ "MintTracker": Object.freeze([217, 230, 22, 187, 250, 88, 11, 174]),
10
+ "NameRegistry": Object.freeze([169, 63, 83, 240, 198, 158, 53, 11]),
11
+ "Review": Object.freeze([124, 63, 203, 215, 226, 30, 222, 15]),
12
+ "ReviewCounter": Object.freeze([89, 31, 82, 96, 68, 42, 80, 60]),
13
+ });
14
+
15
+ module.exports = {
16
+ V3_IDL_ACCOUNT_DISCRIMINATORS,
17
+ };
package/src/v3-pda.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface V3ProgramIds {
11
11
  ESCROW: PublicKey;
12
12
  }
13
13
 
14
+ export const SPL_TOKEN_PROGRAM_ID: PublicKey;
15
+ export const ASSOCIATED_TOKEN_PROGRAM_ID: PublicKey;
16
+ export const V3_DEVNET_TOKEN_MINTS: {
17
+ USDC: PublicKey;
18
+ };
19
+
14
20
  /** Get all V3 program IDs for a network. */
15
21
  export function getV3ProgramIds(network?: Network): V3ProgramIds;
16
22
 
@@ -70,3 +76,23 @@ export function getV3EscrowPDA(
70
76
  nonce: number | bigint,
71
77
  network?: Network
72
78
  ): [PublicKey, number];
79
+
80
+ /** Derive canonical Associated Token Account address. */
81
+ export function getAssociatedTokenAddress(
82
+ owner: PublicKey | string,
83
+ mint: PublicKey | string
84
+ ): [PublicKey, number];
85
+
86
+ /** Derive Escrow V3 PDA plus its SPL vault ATA for a mint. */
87
+ export function getV3EscrowVaultATA(
88
+ client: PublicKey | string,
89
+ descriptionHash: Buffer,
90
+ nonce: number | bigint,
91
+ mint: PublicKey | string,
92
+ network?: Network
93
+ ): {
94
+ escrowPDA: PublicKey;
95
+ escrowBump: number;
96
+ vaultATA: PublicKey;
97
+ vaultBump: number;
98
+ };