actas 0.4.1 → 0.5.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/README.md
CHANGED
|
@@ -11,7 +11,7 @@ the token from your scoped ActAs connection in the Authorization bearer header,
|
|
|
11
11
|
saved through your client's protected credential settings. Do not put the token
|
|
12
12
|
in the endpoint URL, a prompt, or source control. OAuth is not offered.
|
|
13
13
|
|
|
14
|
-
The remote endpoint shares the
|
|
14
|
+
The remote endpoint shares the 21 stdio tool schemas and applies the same API
|
|
15
15
|
permissions and revocation checks on every call. Browser flows still need a
|
|
16
16
|
local browser adapter. Private verification handles and registered executable
|
|
17
17
|
consumers return `needs_local_runtime` remotely; use the local client below.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {randomBytes} from 'node:crypto';
|
|
2
|
+
import {mailContent} from './mail-content.mjs';
|
|
2
3
|
|
|
3
4
|
// Values never leave this trusted runtime through MCP. Handles expire and consume once.
|
|
4
5
|
export class PrivateValues {
|
|
@@ -16,14 +17,13 @@ export function validateInboxArgs(args){
|
|
|
16
17
|
if(args.timeoutMs!==undefined&&(!Number.isInteger(args.timeoutMs)||args.timeoutMs<0||args.timeoutMs>30000))throw Error('Invalid timeout.');return args;
|
|
17
18
|
}
|
|
18
19
|
export function verificationValue(message,args,now=Date.now()){
|
|
20
|
+
const data=mailContent(message);if(!data)return;
|
|
19
21
|
const timestamp=Date.parse(message.timestamp),after=Date.parse(args.after);
|
|
20
|
-
if(!message.labels?.includes('received')||!Number.isFinite(timestamp)||timestamp<after||timestamp>now||senderAddress(
|
|
22
|
+
if(!message.labels?.includes('received')||!Number.isFinite(timestamp)||timestamp<after||timestamp>now||senderAddress(data.from)!==args.sender.toLowerCase()||data.subject!==args.subject||typeof data.text!=='string')return;
|
|
21
23
|
// ActAs wraps received mail as untrusted; match only the body, and only
|
|
22
24
|
// when ActAs authenticated the sender's domain.
|
|
23
25
|
if(message.sender_check?.domain_verified!==true)return;
|
|
24
|
-
const
|
|
25
|
-
if(message.untrusted&&!wrapped)return;
|
|
26
|
-
const body=(wrapped?wrapped[2]:message.text).slice(0,200*1024);
|
|
26
|
+
const body=data.text.slice(0,200*1024);
|
|
27
27
|
if(args.codePattern){const re=new RegExp(`(?:^|[^A-Za-z0-9])(${args.codePattern})(?=$|[^A-Za-z0-9])`),match=re.exec(body);return match?.[1];}
|
|
28
28
|
for(const candidate of body.match(/https:\/\/[^\s<>"']+/g)??[]){try{const url=new URL(candidate);if(url.origin===args.linkOrigin&&url.pathname===args.linkPath&&!url.username&&!url.password&&!url.hash)return url.href;}catch{}}
|
|
29
29
|
}
|
|
@@ -92,6 +92,8 @@ export function createAccountsRuntime({request,codesRequest=request,scope,identi
|
|
|
92
92
|
const result=await codesRequest('/emails/'+encodeURIComponent(args.inboxId)+'/code','POST',{sender:args.sender,subject:args.subject,after:args.after,codePattern:args.pattern??'\\d{6}',timeoutMs:args.timeoutMs??30000});
|
|
93
93
|
return result.status==='matched'?{status:'matched',code:result.code,receivedAt:result.receivedAt}:{status:'pending'};
|
|
94
94
|
}
|
|
95
|
+
case 'actas_passkey_register':return request('/vault/passkeys/register','POST',{...args,...scope});
|
|
96
|
+
case 'actas_passkey_sign':return request('/vault/passkeys/sign','POST',{...args,...scope});
|
|
95
97
|
case 'actas_vault_login':return vaultLogin(request,scope,args.path);
|
|
96
98
|
case 'actas_totp_code':return request('/vault/read','POST',{...scope,path:args.path,action:'totp:code'});
|
|
97
99
|
case 'actas_identity_accounts':{const id=args.identityId??identityId;if(!id)throw Error('Choose an identity for this registry.');return request('/identities/'+encodeURIComponent(id)+'/accounts','GET');}
|
|
@@ -8,6 +8,8 @@ export function createAccountsMcp(runtime){
|
|
|
8
8
|
const definitions=[
|
|
9
9
|
['actas_identity_create','Create an identity with a permanently reserved chosen email name (management). inbox:true also allocates its inbox.',{name:z.string().min(1).max(80),localPart:z.string().min(3).max(40).optional(),domain:z.string().min(3).max(253).optional(),inbox:z.boolean().optional()}],
|
|
10
10
|
['actas_inbox_wait_for_code','Wait up to 60s for one sign-in code from an authenticated sender (emails:codes). Returns only the code.',{inboxId:z.string().min(1).max(160),sender:z.string().email().max(254),subject:z.string().min(1).max(200),after:z.string().datetime(),pattern:z.string().regex(/^(?:\\d|\[A-Z0-9\])\{(?:[4-9]|1[0-2])\}$/).optional(),timeoutMs:z.number().int().min(0).max(60000).optional()}],
|
|
11
|
+
['actas_passkey_register','Create an ES256 passkey for a granted login; private key stays encrypted in hosted ActAs. Requires read, reveal and write grants.',{path,rpId:z.string().min(3).max(253),origin:z.string().url(),challenge:z.string().max(1368),userHandle:z.string().max(86),userVerification:z.enum(['required','preferred','discouraged']).optional()}],
|
|
12
|
+
['actas_passkey_sign','Sign a WebAuthn assertion for a granted login; private key never leaves hosted ActAs. Requires read and reveal grants.',{path,rpId:z.string().min(3).max(253),origin:z.string().url(),challenge:z.string().max(1368),credentialId:z.string().max(128).optional(),allowCredentials:z.array(z.string().max(128)).max(64).optional(),userVerification:z.enum(['required','preferred','discouraged']).optional()}],
|
|
11
13
|
['actas_vault_login','Return a granted login item: username/email, password and a current TOTP code. Never the TOTP seed (secrets:reveal plus read, reveal and totp:code grants).',{path}],
|
|
12
14
|
['actas_domains_list','List workspace domains and exact DNS records (domain management).',{}],
|
|
13
15
|
['actas_domains_add','Claim a workspace mail domain (domain management); reconcile by checking the same domain.',{domain:z.string().min(3).max(253)}],
|
|
@@ -2,14 +2,18 @@ import {readFile} from 'node:fs/promises';
|
|
|
2
2
|
import {ActAs} from '../scripts/proof-client.mjs';
|
|
3
3
|
import {loadMappings,selectMapping,callerExecutor} from './caller.mjs';
|
|
4
4
|
import {helper} from './helper.mjs';
|
|
5
|
+
import {mailContent} from './mail-content.mjs';
|
|
5
6
|
|
|
6
7
|
// Provider messages are untrusted. Only exact owner-assigned senders, recipients,
|
|
7
8
|
// verification paths and recent received messages can supply a challenge.
|
|
8
9
|
export function emailChallenge(message,config,principal,since,now=Date.now()) {
|
|
10
|
+
const data=mailContent(message);if(!data)return;
|
|
11
|
+
if(message.untrusted&&message.sender_check?.domain_verified!==true)return;
|
|
9
12
|
const timestamp=Date.parse(message.timestamp);
|
|
10
13
|
if(!Number.isFinite(since)||since>now||!Number.isFinite(timestamp)||timestamp<since||timestamp>now||now-timestamp>900000||!message.labels?.includes('received'))return;
|
|
11
14
|
const address=value=>typeof value==='string'?value.match(/<([^<>]+)>$/)?.[1]||value:undefined;
|
|
12
|
-
if(!config.senders?.includes(address(
|
|
15
|
+
if(!config.senders?.includes(address(data.from))||!Array.isArray(data.to)||!data.to.some(v=>address(v)===principal)||typeof data.text!=='string')return;
|
|
16
|
+
message={...message,text:data.text};
|
|
13
17
|
if(config.kind==='code'){
|
|
14
18
|
// A fixed label avoids arbitrary regex execution and ambiguous numbers in mail.
|
|
15
19
|
if(typeof config.label!=='string'||!config.label||!Number.isInteger(config.digits)||config.digits<4||config.digits>10)return;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Trusted consumers may parse outside data for display/exact challenge matching.
|
|
2
|
+
// Never return this decoded object directly through an agent-facing mail tool.
|
|
3
|
+
export function mailContent(message){
|
|
4
|
+
if(message?.untrusted!==true)return message;
|
|
5
|
+
if(typeof message.text!=='string')return;
|
|
6
|
+
const match=/^<(untrusted-email-[a-f0-9]{16})>\nOutside email\.[^\n]*\nSender check: [^\n]*\nMetadata: ([^\n]*)\n\n([\s\S]*)\n<\/\1>$/.exec(message.text);
|
|
7
|
+
if(!match)return;
|
|
8
|
+
try{const metadata=JSON.parse(match[2]);if(!metadata||typeof metadata!=='object'||Array.isArray(metadata))return;return {...metadata,text:match[3]};}catch{return;}
|
|
9
|
+
}
|