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 19 stdio tool schemas and applies the same API
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(message.from)!==args.sender.toLowerCase()||typeof message.subject!=='string'||!message.subject.includes(args.subject)||typeof message.text!=='string')return;
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 wrapped=message.untrusted?/^<(untrusted-email-[a-f0-9]+)>\n[\s\S]*?\n\n([\s\S]*)\n<\/\1>$/.exec(message.text):null;
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(message.from))||!Array.isArray(message.to)||!message.to.some(v=>address(v)===principal)||typeof message.text!=='string')return;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actas",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "mcpName": "dev.actas/accounts",
5
5
  "type": "module",
6
6
  "description": "Persistent identity, email, saved accounts and scoped credentials for AI agents",