diff --git a/clients/typescript/network/README.md b/clients/typescript/network/README.md index b8ed1df..4f2d260 100644 --- a/clients/typescript/network/README.md +++ b/clients/typescript/network/README.md @@ -332,3 +332,44 @@ history. Re-registering a completed selection can reuse authenticated local history without another upload. Direct `return_mailbox_receipt` supports the same message and source selection. Native replica inbox support remains unfinished. These additions are development source after the frozen alpha.0.36 candidate. + +### Receiver-issued mailbox authorization (development after alpha.0.37) + +After registering an already provisioned mailbox and explicitly approving the +sender's contact request, a native recipient can export the existing mailbox +admission invitation with ordinary `connect`: + +```ts +await agent.handle({op: 'connect', invitation: { + schema_version: 'memory-vault-open-mailbox-connect/v1', action: 'authorize', + receiver_id, contact_request_ref, expires_at, status_revision, status_until +}}); +``` + +Choose the expiry windows within the original contact and mailbox permissions. +The owner supplies and coordinates `status_revision`; a new selection must not +reuse or roll back an issued revision. Authorization binds the exact approved +contact, sender and recipient keys, mailbox slot, selected source and existing +storage resources. It does not reserve new storage or create ACK-return grants. +The native client signs with the recipient's own existing identity and persists +the exact destination and status originals before returning them. + +The response supplies a base64 `authorization_chunk` of at most 3,072 bytes, +`authorization_sha256`, `total_bytes`, `offset`, and `next_cursor`. Repeat the +same invitation with `cursor: next_cursor` until it is null, join decoded chunks +in order, and verify the complete byte count and SHA-256 before parsing the +sender's `retain` authorization. A cursor cannot be reused for different bytes. +`inspect` with `receiver_id` similarly pages `configuration_chunk` with +`configuration_sha256`; both operations reauthenticate the retained receiver +and perform no HTTP requests. + +Python and native clients share the bounded destination journal. Restarting or +switching clients returns the same signed originals; a conflicting request or +reused older revision fails. Both clients reauthenticate retained receive-status +observations inside the signing transaction. Revoked slot, parent permission or +storage-resource authority blocks both cached output and fresh issuance even +after that status expires; higher remembered document floors also remain in force. A completed local authorization does not prove that +the source is currently available or that a message was delivered. The sender +must still prepare and admit its exact ciphertext under current source checks; +these sender and provisioning operations currently use Python. The existing +native mailbox receive and independent receipt-return paths remain available. diff --git a/clients/typescript/network/open-contact-client.ts b/clients/typescript/network/open-contact-client.ts index bbcc9ad..2f79805 100644 --- a/clients/typescript/network/open-contact-client.ts +++ b/clients/typescript/network/open-contact-client.ts @@ -57,6 +57,13 @@ export class OpenContactClient{ if(!row||Buffer.from(row.body).equals(Buffer.from(LOCAL_RESERVATION)))throw new ContactError('contact_local_missing'); if(row.expires_at<=now())throw new ContactError('contact_expired');return document(row.body,24576); } + approvedIncomingOriginals(reference:string):Obj{ + const incoming=this.load('incoming',reference),decision=this.load('decision',reference).decision; + if(decision.payload.decision!=='approved')throw new ContactError('open_delivery_not_authorized'); + const grant=decision.payload.grant,docs={node:incoming.node,policy:incoming.policy,request:incoming.request, + decision,knock_lease:incoming.lease,grant,delivery_lease:grant.payload.resource_lease}; + return Object.fromEntries(Object.entries(docs).map(([name,value])=>[name,canonicalBytes(value)])); + } private savePolicy(reservationRef:string,leaseId:string,value:Obj,expires:number):void{ const raw=canonicalBytes(document(value,24576)); this.participant.contactStorage(db=>transaction(db,()=>{ diff --git a/clients/typescript/network/open-mailbox-destination.ts b/clients/typescript/network/open-mailbox-destination.ts new file mode 100644 index 0000000..c7fab85 --- /dev/null +++ b/clients/typescript/network/open-mailbox-destination.ts @@ -0,0 +1,99 @@ +/** Receiver-issued original mailbox admission, retained across client changes. */ +import {canonicalBytes,sha256,encodeBase64url,decodeBase64url,validateSigningIdentity,validateEncryptionIdentity} from './crypto.ts'; +import type {EncryptionIdentityDocument} from './crypto.ts'; +import type {OpenParticipant} from './open-participant.ts'; +import {transaction} from './io.ts'; +import {RepairBudget,RepairError,buildNewWire,parseNewWire,objectFields,rawRef,u53} from './open-repair-wire.ts'; +import {DEFAULT_REPAIR_CLIENT_POLICY} from './open-repair-client.ts'; +import {verifyMailboxFeedBootstrap} from './open-repair-mailbox-authority.ts'; +import {originalPublicDescriptor,verifyContactOriginals,verifyBoundedControlSignature} from './open-repair-original.ts'; +import {signBoundedBootstrapOriginal} from './open-repair-probe.ts'; +import {MailboxSetupJournal} from './open-mailbox-journal.ts'; +import {authenticateStatusOriginal,statusScope} from './open-repair-status.ts'; +import {issueStatus} from './open-provider.ts'; +type Obj=Record; +function fail(code:string):never{throw new RepairError(code);} +/** Inputs are the locally reauthenticated receiver and approved contact originals. */ +export function prepareMailboxDestination(participant:OpenParticipant,encryption:EncryptionIdentityDocument,config:Obj,slots:Obj,contact:Obj, + value:{expires_at:number;status_revision:number;status_until:number}):Obj{ + objectFields(value,['expires_at','status_revision','status_until']); + const policy={...DEFAULT_REPAIR_CLIENT_POLICY,max_signature_checks:512},budget=new RepairBudget(policy),at=Math.floor(Date.now()/1000); + const owner={signing_key:validateSigningIdentity(participant.identity),encryption_key:validateEncryptionIdentity(encryption)}; + const encode=(entry:Obj)=>{const ref=rawRef(entry.ref),raw=parseNewWire(entry.raw,policy,budget).raw;if(raw.length!==ref.size||budget.hash(raw)!==ref.raw_sha256)fail('repair_ref_mismatch');return {ref,raw_base64url:encodeBase64url(raw)};}; + const decode=(entry:unknown)=>{const e=objectFields(entry,['ref','raw_base64url']),ref=rawRef(e.ref),raw=decodeBase64url(e.raw_base64url,65536);if(raw.length!==ref.size||budget.hash(raw)!==ref.raw_sha256)fail('repair_ref_mismatch');return {ref,raw};}; + const setup=verifyMailboxFeedBootstrap(slots,{expectedSlot:config.expected_slot,expectedOwner:owner,expectedTarget:config.expected_target, + targetStorageEpoch:config.expected_slot.writer_storage_epoch,limitPolicy:config.limit_policy,at,policy,budget}),slot=setup.slot.payload as Obj,root=slot.slot_key.root_key; + const sender=objectFields(config.expected_sender,['signing_key','encryption_key']); + originalPublicDescriptor(sender.signing_key,budget);originalPublicDescriptor(sender.encryption_key,budget,true); + if(sender.signing_key.key_id!==slot.sender.signing_key_id||sender.encryption_key.key_id!==slot.sender.encryption_key_id)fail('open_invalid_mailbox_receiver'); + const plan={root_key:root,slot_key:slot.slot_key,sender:slot.sender,target:config.expected_target,limits:config.limit_policy, + ...Object.fromEntries(['budget','windows','max_appends','max_live_items'].map(n=>[n,slot[n]]))}; + u53(value.status_revision,1);u53(value.expires_at);u53(value.status_until); + const binding=budget.hash(buildNewWire({plan,slots:Object.fromEntries(Object.entries(slots).map(([n,e])=>[n,encode(e)])), + contact:Object.fromEntries(Object.entries(contact).map(([n,raw])=>[n,budget.hash(raw as Uint8Array)])),...value},policy,budget).raw); + const rootDigest=budget.hash(buildNewWire(root,policy,budget).raw); + const journal=new MailboxSetupJournal(participant),journalKey=journal.start({kind:'mailbox.feed_recovery',slot_key:slot.slot_key,owner, + sender:config.expected_sender,target:config.expected_target,entries:Object.fromEntries(Object.entries(setup).map(([n,e])=>[n,e.ref]))}); + const required=new Map(); + for(const [name,mask] of [['slot',65],['read',2],['maintenance',65],['bootstrap',10]] as const){ + const e=setup[name],kind=name==='slot'?'mailbox_slot':'authority',scope=statusScope(root,kind,name==='slot'?slot.slot_key:{authority_kind:e.payload.kind,authority_sha256:e.ref.raw_sha256},policy,budget); + required.set(owner.signing_key.key_id+':'+kind+':'+scope,{revision:e.payload.revision as number,mask}); + } + for(const name of ['data','metadata'])required.set(config.expected_target.signing_key.key_id+':resource:'+statusScope(root,'resource',slot[name+'_resource_ref'],policy,budget),{revision:Infinity,mask:65}); + return participant.providerStorage(db=>transaction(db,()=>{ + const revisions=new Map();let observedOwnerRevision=-1; + for(const entry of journal.statuses(journalKey)){ + const signed=objectFields(parseNewWire(entry.raw,policy,budget).value,['payload','proof']),preview=signed.payload as Obj; + const signer=[owner.signing_key,config.expected_target.signing_key,config.expected_sender.signing_key].find(v=>v.key_id===preview.signing_key?.key_id); + if(!signer||u53(preview.issued_at)>at||!Array.isArray(preview.entries)||preview.entries.length>16)fail('repair_status_disclosure'); + const item=authenticateStatusOriginal(entry,{expectedRoot:root,expectedSigningKey:signer,at:preview.issued_at, + allowedScopes:preview.entries.map((e:Obj)=>({scope_kind:e.scope_kind,scope_id:e.scope_id})),policy,budget}); + const status=item.payload as Obj,key=signer.key_id+':'+status.revision; + if(revisions.has(key)&&revisions.get(key)!==item.canonical_sha256)fail('repair_status_conflict');revisions.set(key,item.canonical_sha256); + if(signer.key_id===owner.signing_key.key_id)observedOwnerRevision=Math.max(observedOwnerRevision,status.revision); + for(const row of status.entries){const need=required.get(signer.key_id+':'+row.scope_kind+':'+row.scope_id);if(!need)continue; + if(row.status==='revoked'&&(row.operation_mask&need.mask))fail('repair_authority_revoked'); + if(row.minimum_document_revision>need.revision)fail('repair_status_revision'); + } + } + db.exec('CREATE TABLE IF NOT EXISTS open_mailbox_destinations(root_digest TEXT NOT NULL,revision INTEGER NOT NULL,binding TEXT NOT NULL,bundle BLOB NOT NULL,bundle_sha256 TEXT NOT NULL,PRIMARY KEY(root_digest,revision))'); + const old=db.prepare('SELECT binding,bundle,bundle_sha256 FROM open_mailbox_destinations WHERE root_digest=? AND revision=?').get(rootDigest,value.status_revision) as Obj|undefined; + if(old){ + if(old.binding!==binding)fail('repair_destination_conflict');if(old.bundle.length>65536||budget.hash(old.bundle)!==old.bundle_sha256)fail('repair_destination_journal_corrupt'); + const saved=objectFields(parseNewWire(old.bundle,policy,budget).value,['destination','owner_status']),result=Object.fromEntries(Object.entries(saved).map(([n,e])=>[n,decode(e)])); + for(const entry of Object.values(result)){const signed=objectFields(parseNewWire(entry.raw,policy,budget).value,['payload','proof']);verifyBoundedControlSignature(signed.payload,signed.proof,owner.signing_key,budget);} + return result; + } + const latest=(db.prepare('SELECT max(revision) AS revision FROM open_mailbox_destinations WHERE root_digest=?').get(rootDigest) as Obj).revision; + if(value.status_revision<=observedOwnerRevision||(latest!==null&&value.status_revision<=latest))fail('repair_destination_revision_rollback'); + if((db.prepare('SELECT count(*) AS n FROM open_mailbox_destinations').get() as Obj).n>=128)fail('repair_destination_capacity'); + const held=verifyContactOriginals(contact,{senderKeyId:slot.sender.signing_key_id,senderEncryptionKeyId:slot.sender.encryption_key_id, + recipientKeyId:owner.signing_key.key_id,recipientEncryptionKeyId:owner.encryption_key.key_id,nodeKeyId:config.expected_target.signing_key.key_id, + storageEpoch:slot.slot_key.writer_storage_epoch,at,policy,budget}); + if(!(atsetup[n].payload.expires_at as number),held.originals.grant.payload.expires_at as number)) + ||!(ate.payload.expires_at as number))))fail('repair_resource_expired'); + const refs=Object.fromEntries([['contact_request_ref','request'],['contact_policy_ref','policy'],['contact_knock_lease_ref','knock_lease'],['contact_decision_ref','decision'],['store_grant_ref','grant']].map(([field,role])=>{ + const raw=held.originals[role].document.raw,digest=budget.hash(raw);return [field,{namespace:'meta',key:digest,raw_sha256:digest,size:raw.length}];})); + const destinationId='destination_'+sha256(Buffer.concat([Buffer.from('memory-vault-mailbox-setup/v1\0'),buildNewWire(root,policy,budget).raw,Buffer.from('\0destination')])); + const signed=signBoundedBootstrapOriginal({schema_version:'memory-vault-open-repair/v1',kind:'delivery.destination',signing_key:owner.signing_key, + issued_at:at,expires_at:value.expires_at,destination_id:destinationId,sender:slot.sender,recipient:slot.recipient,slot_key:slot.slot_key,slot_ref:setup.slot.ref,...refs, + ...Object.fromEntries(['data_resource_ref','data_resource_offer_ref','metadata_resource_ref','metadata_resource_offer_ref','read_grant_ref','maintenance_root_ref','budget','windows'].map(n=>[n,slot[n]]))},participant.identity,policy,budget); + const destination={raw:signed.raw,ref:signed.ref},scopes=[['destination',destination],...Object.entries(slots)].map(([name,entry])=>{ + const e=entry as Obj,p=(parseNewWire(e.raw,policy,budget).value as Obj).payload,kind=name==='slot'?'mailbox_slot':'authority'; + return {scope_kind:kind,scope_id:statusScope(root,kind,name==='slot'?slot.slot_key:{authority_kind:p.kind,authority_sha256:e.ref.raw_sha256},policy,budget), + minimum_document_revision:p.revision??1,status:'active',operation_mask:127}; + }).sort((a,b)=>a.scope_kindb.scope_kind?1:a.scope_idb.scope_id?1:0); + const raw=buildNewWire(issueStatus(participant.identity,{root,revision:value.status_revision,entries:scopes,issued_at:at,valid_until:value.status_until}),policy,budget).raw,digest=budget.hash(raw); + const result={destination,owner_status:{raw,ref:{namespace:'meta',key:digest,raw_sha256:digest,size:raw.length}}}; + const bundle=buildNewWire(Object.fromEntries(Object.entries(result).map(([n,e])=>[n,encode(e)])),policy,budget).raw;if(bundle.length>65536)fail('repair_destination_capacity'); + db.prepare('INSERT INTO open_mailbox_destinations VALUES(?,?,?,?,?)').run(rootDigest,value.status_revision,binding,bundle,budget.hash(bundle));return result; + })); +} +/** Finite output shares the existing Python cursor and exact authorization bytes. */ +export function mailboxPage(value:unknown,receiver:string,cursor:unknown,kind:'authorization'|'configuration'):Obj{ + const raw=canonicalBytes(value,65536),digest=sha256(raw);let offset=0; + if(cursor!==undefined&&cursor!==null){const v=objectFields(cursor,['sha256','offset']);offset=v.offset;if(v.sha256!==digest||!Number.isSafeInteger(offset)||offset<=0||offset>=raw.length||offset%3072)fail('open_invalid_mailbox_cursor');} + const end=Math.min(offset+3072,raw.length);return {state:'mailbox_'+kind,receiver_id:receiver,[kind+'_sha256']:digest,total_bytes:raw.length,offset, + [kind+'_chunk']:Buffer.from(raw.subarray(offset,end)).toString('base64'),next_cursor:end===raw.length?null:{sha256:digest,offset:end}, + network_accessed:false,source_rechecked:false,receipt_return:'separate_authority_required'}; +} diff --git a/clients/typescript/network/open-mailbox-receivers.ts b/clients/typescript/network/open-mailbox-receivers.ts index faf0b7b..d0bcf31 100644 --- a/clients/typescript/network/open-mailbox-receivers.ts +++ b/clients/typescript/network/open-mailbox-receivers.ts @@ -12,6 +12,8 @@ import {verifyMailboxFeedBootstrap} from './open-repair-mailbox-authority.ts'; import {verifySourceNodeOriginal} from './open-repair-original.ts'; import {endpoint} from './open-transport.ts'; import {transaction} from './io.ts'; +import {prepareMailboxDestination,mailboxPage} from './open-mailbox-destination.ts'; +import {OpenContactClient} from './open-contact-client.ts'; import {MailboxReceiptJobs} from './open-mailbox-receipts.ts'; type Obj=Record; export const MAILBOX_CONNECT_SCHEMA='memory-vault-open-mailbox-connect/v1'; @@ -40,6 +42,20 @@ export class RegisteredMailboxReceivers{ const value=document(invitation as any,65536) as Obj;if(value.schema_version!==MAILBOX_CONNECT_SCHEMA)fail('open_invalid_mailbox_receiver'); if(value.action==='list'){objectFields(value,['schema_version','action']);const rows=this.#participant.providerStorage(db=>db.prepare('SELECT receiver_id FROM open_mailbox_receivers ORDER BY receiver_id LIMIT 17').all()) as Obj[];if(rows.length>16)fail('open_mailbox_receiver_capacity');return {state:'configured',mailboxes:rows.map(v=>v.receiver_id),network_accessed:false};} if(value.action==='remove'){objectFields(value,['schema_version','action','receiver_id']);opaqueId(value.receiver_id);this.#participant.providerStorage(db=>db.prepare('DELETE FROM open_mailbox_receivers WHERE receiver_id=?').run(value.receiver_id));return {state:'removed',receiver_id:value.receiver_id,network_accessed:false};} + if(value.action==='inspect'||value.action==='authorize'){ + objectFields(value,['schema_version','action','receiver_id',...(value.action==='authorize'?['contact_request_ref','expires_at','status_revision','status_until']:[]),...(Object.hasOwn(value,'cursor')?['cursor']:[])]); + opaqueId(value.receiver_id);const row=this.#participant.providerStorage(db=>db.prepare('SELECT body FROM open_mailbox_receivers WHERE receiver_id=?').get(value.receiver_id)) as Obj|undefined; + if(!row)fail('open_mailbox_receiver_missing');const config=document(row.body,65536) as Obj; + if(value.receiver_id!=='mailbox_'+sha256(canonicalBytes(config.expected_slot)))fail('open_invalid_mailbox_receiver');const options=this.#validate(config); + if(value.action==='inspect')return mailboxPage(config,value.receiver_id,value.cursor,'configuration'); + const contact=new OpenContactClient(this.#participant,this.#encryption).approvedIncomingOriginals(value.contact_request_ref); + const bundle=prepareMailboxDestination(this.#participant,this.#encryption,config,options.slotEntries,contact, + {expires_at:value.expires_at,status_revision:value.status_revision,status_until:value.status_until}); + const encoded=(e:Obj)=>({raw:Buffer.from(e.raw).toString('utf8'),ref:e.ref}); + return mailboxPage({destination_entry:encoded(bundle.destination),owner_status_entry:encoded(bundle.owner_status), + slot_entries:Object.fromEntries(['slot','read','maintenance'].map(n=>[n,config.slot_entries[n]])),target:config.expected_target, + target_node_entry:config.target_node_entry,base_url:config.base_url},value.receiver_id,value.cursor,'authorization'); + } this.#validate(value);const receiver='mailbox_'+sha256(canonicalBytes(value.expected_slot)),raw=canonicalBytes(value); this.#participant.providerStorage(db=>transaction(db,()=>{const old=db.prepare('SELECT body FROM open_mailbox_receivers WHERE receiver_id=?').get(receiver) as Obj|undefined; if(old){if(!Buffer.from(old.body).equals(Buffer.from(raw)))fail('open_mailbox_receiver_conflict');return;} diff --git a/clients/typescript/network/package.json b/clients/typescript/network/package.json index 41aaa86..be3a478 100644 --- a/clients/typescript/network/package.json +++ b/clients/typescript/network/package.json @@ -50,7 +50,8 @@ "./open-mailbox-client": "./open-mailbox-client.ts", "./open-mailbox-journal": "./open-mailbox-journal.ts", "./open-mailbox-receivers": "./open-mailbox-receivers.ts", - "./open-mailbox-receipts": "./open-mailbox-receipts.ts" + "./open-mailbox-receipts": "./open-mailbox-receipts.ts", + "./open-mailbox-destination": "./open-mailbox-destination.ts" }, "types": "./crypto.ts", "engines": { diff --git a/docs/OPEN_NETWORK_CONTACT.md b/docs/OPEN_NETWORK_CONTACT.md index 783ca92..76b3b20 100644 --- a/docs/OPEN_NETWORK_CONTACT.md +++ b/docs/OPEN_NETWORK_CONTACT.md @@ -197,6 +197,13 @@ signed destination and slot controls; the sender reuses its own frozen outbox ciphertext and contact originals. Preparation never invents permission or sends plaintext to the mailbox node. +Native TypeScript also supports receiver `authorize` and paginated `inspect` in +development source after alpha.0.37. It shares the exact destination journal with +Python, signs only with the receiver's own existing identity, and performs no HTTP +requests during authorization. Both clients apply authenticated retained +revocations before returning or issuing authorization. Source provisioning and +sender mailbox admission still use Python. + A receiver using the Agent interface can authorize from an existing registered mailbox and its locally approved contact, without opening a database or loading private key objects in the calling program: diff --git a/memory_vault.py b/memory_vault.py index d7b0e14..b8e6782 100644 --- a/memory_vault.py +++ b/memory_vault.py @@ -60,7 +60,7 @@ from typing import Any, Callable, Iterable, Mapping, Sequence -VERSION = "0.28.0-alpha.0.37" +VERSION = "0.28.0-alpha.0.38" REQUEST_SCHEMA = "universal-agent-memory-request/v1" RESULT_SCHEMA = "universal-agent-memory-result/v1" RECORD_SCHEMA = "universal-memory-record/v1" diff --git a/memory_vault_open_repair_client.py b/memory_vault_open_repair_client.py index 7b41212..e939b76 100644 --- a/memory_vault_open_repair_client.py +++ b/memory_vault_open_repair_client.py @@ -1348,6 +1348,47 @@ class MailboxDestinationStore: def __init__(self, db, identity, encryption_identity, *, policy=DEFAULT_POLICY): self.db,self.identity,self.encryption_identity,self.policy=db,identity,encryption_identity,policy + def _retained_authority(self, builder, slots, contact, at, budget): + """Authenticate durable receive observations before issuing more authority.""" + from memory_vault_open_repair_mailbox_activation import verify_mailbox_feed_bootstrap + p=builder.plan;root=p['root_key'];owner=builder.owner;target=p['target'] + request=original.parse_original_control(contact['request'],self.policy,budget).value['payload'] + sender=dict(signing_key=request['signing_key'],encryption_key=request['encryption_key']) + plan=wire.build_new_wire(dict(kind='mailbox.feed_recovery',slot_key=p['slot_key'],owner=owner,sender=sender,target=target, + entries={name:value['ref'] for name,value in slots.items()}),self.policy,budget).raw + key=budget._hash(plan) + exists=self.db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='open_mailbox_setup_jobs'").fetchone() + if exists is None or self.db.execute('SELECT 1 FROM open_mailbox_setup_jobs WHERE job_key=?',(key,)).fetchone() is None:return -1 + setup=verify_mailbox_feed_bootstrap(slots,expected_slot=p['slot_key'],expected_owner=owner, + expected_target=target,target_storage_epoch=p['slot_key']['writer_storage_epoch'], + limit_policy=p['limits'],at=at,policy=self.policy,budget=budget) + required={} + for name,bits in (('slot',65),('read',2),('maintenance',65),('bootstrap',10)): + entry=setup[name];kind='mailbox_slot' if name=='slot' else 'authority' + subject=p['slot_key'] if name=='slot' else dict(authority_kind=entry.payload['kind'],authority_sha256=entry.ref.raw_sha256) + required[(owner['signing_key']['key_id'],kind,status.status_scope(root,kind,subject,self.policy,budget))]=(entry.payload['revision'],bits) + for name in ('data','metadata'): + resource=setup['slot'].payload[name+'_resource_ref'] + required[(target['signing_key']['key_id'],'resource',status.status_scope(root,'resource',resource,self.policy,budget))]=(None,65) + revisions={};owner_revision=-1 + for entry in MailboxSetupJournal(self.db).statuses(key): + preview=original.parse_original_control(entry['raw'],self.policy,budget).value['payload'] + signer=next((v['signing_key'] for v in (owner,target,sender) if v['signing_key']==preview['signing_key']),None) + if signer is None or wire.u53(preview['issued_at'])>at:_fail('repair_status_disclosure') + observed=status.authenticate_status_original(entry,expected_root=root,expected_signing_key=signer,at=preview['issued_at'], + allowed_scopes=[dict(scope_kind=v['scope_kind'],scope_id=v['scope_id']) for v in preview['entries']],policy=self.policy,budget=budget) + value=observed.payload;identity=(signer['key_id'],value['revision']) + if identity in revisions and revisions[identity]!=observed.canonical_sha256:_fail('repair_status_conflict') + revisions[identity]=observed.canonical_sha256 + if signer==owner['signing_key']:owner_revision=max(owner_revision,value['revision']) + for row in value['entries']: + needed=required.get((signer['key_id'],row['scope_kind'],row['scope_id'])) + if needed is None:continue + revision,bits=needed + if row['status']=='revoked' and row['operation_mask']&bits:_fail('repair_authority_revoked') + if revision is not None and row['minimum_document_revision']>revision:_fail('repair_status_revision') + return owner_revision + def prepare(self, plan, slot_entries, contact_originals, *, at, expires_at, status_revision, status_until): from memory_vault_open_repair_bind import encode_entry,decode_entry import memory_vault_open_repair_resource as resource @@ -1365,6 +1406,7 @@ def prepare(self, plan, slot_entries, contact_originals, *, at, expires_at, stat if self.db.in_transaction:_fail('repair_destination_transaction_active') self.db.execute('BEGIN IMMEDIATE') try: + observed_revision=self._retained_authority(builder,slots,contact_originals,at,budget) self.db.execute('CREATE TABLE IF NOT EXISTS open_mailbox_destinations(root_digest TEXT NOT NULL,revision INTEGER NOT NULL,binding TEXT NOT NULL,bundle BLOB NOT NULL,bundle_sha256 TEXT NOT NULL,PRIMARY KEY(root_digest,revision))') row=self.db.execute('SELECT binding,bundle,bundle_sha256 FROM open_mailbox_destinations WHERE root_digest=? AND revision=?',(root,status_revision)).fetchone() if row is not None: @@ -1378,7 +1420,7 @@ def prepare(self, plan, slot_entries, contact_originals, *, at, expires_at, stat original._verify_control_signature(signed['payload'],signed['proof'],builder.owner['signing_key'],budget) else: latest=self.db.execute('SELECT max(revision) FROM open_mailbox_destinations WHERE root_digest=?',(root,)).fetchone()[0] - if latest is not None and status_revision<=latest:_fail('repair_destination_revision_rollback') + if status_revision<=observed_revision or (latest is not None and status_revision<=latest):_fail('repair_destination_revision_rollback') if self.db.execute('SELECT count(*) FROM open_mailbox_destinations').fetchone()[0]>=128:_fail('repair_destination_capacity') result=builder.destination_bundle(slots,contact_originals,at=at,expires_at=expires_at, status_revision=status_revision,status_until=status_until) diff --git a/packaging/CLIENT_README.md b/packaging/CLIENT_README.md index 0fa2a7f..0012950 100644 --- a/packaging/CLIENT_README.md +++ b/packaging/CLIENT_README.md @@ -1,4 +1,13 @@ -# Memory Vault v0.28.0-alpha.0.37 — authorized full client +# Memory Vault v0.28.0-alpha.0.38 — authorized full client + +Native TypeScript recipients can issue an original mailbox destination from +an explicitly approved contact and a registered mailbox. The receiver signs with +its own existing identity and exports bounded authorization pages for sender +admission. Python and native clients reopen the same persisted signed originals; +conflicting selections and stale revisions fail. Both clients enforce retained +parent/resource revocations before cached or fresh authorization, even after the +status expires. Authorization creates no new storage or receipt-return grants. +See `clients/typescript/network/README.md` and `docs/OPEN_NETWORK_CONTACT.md`. Native TypeScript recipients can retain an independently authorized receipt return for a cold-mailbox message. Ordinary Agent `receive` then returns the @@ -157,7 +166,7 @@ remain unfinished. There is no project-operated public seed or verified thousand-agent/global-availability result. Use the source-bound release record for actual validation and compare archive bytes with published checksums. -This full-client package targets **v0.28.0-alpha.0.37 open-delivery source**, +This full-client package targets **v0.28.0-alpha.0.38 open-delivery source**, not a stable-release or complete runtime-certification claim. Existing published versions remain immutable. Match the artifact's source and hashes to its manifest; this README does not establish installation or publication. The plugin is under diff --git a/packaging/PROTOCOL_README.md b/packaging/PROTOCOL_README.md index 4c230e7..c09dd74 100644 --- a/packaging/PROTOCOL_README.md +++ b/packaging/PROTOCOL_README.md @@ -1,4 +1,13 @@ -# Memory Vault v0.28.0-alpha.0.37 — independent protocol +# Memory Vault v0.28.0-alpha.0.38 — independent protocol + +Native TypeScript recipients can issue an original mailbox destination from +an explicitly approved contact and a registered mailbox. The receiver signs with +its own existing identity and exports bounded authorization pages for sender +admission. Python and native clients reopen the same persisted signed originals; +conflicting selections and stale revisions fail. Both clients enforce retained +parent/resource revocations before cached or fresh authorization, even after the +status expires. Authorization creates no new storage or receipt-return grants. +See `clients/typescript/network/README.md` and `docs/OPEN_NETWORK_CONTACT.md`. Native TypeScript recipients can retain an independently authorized receipt return for a cold-mailbox message. Ordinary Agent `receive` then returns the @@ -174,7 +183,7 @@ require independently configured providers; reading metadata cannot grant authority or enroll keys. The complete Python client and executable synthetic review kit are separate -artifacts described in `docs/RELEASE.md`. This package targets v0.28.0-alpha.0.37; +artifacts described in `docs/RELEASE.md`. This package targets v0.28.0-alpha.0.38; previous published versions remain immutable. The optional native network adds communication around existing records without changing canonical record/v1 or share-v1. It has no MCP, A2A, Matrix, Nostr or Graphiti adapter or compatibility diff --git a/packaging/RELEASE_NOTES.md b/packaging/RELEASE_NOTES.md index 4b49e7a..d6be29d 100644 --- a/packaging/RELEASE_NOTES.md +++ b/packaging/RELEASE_NOTES.md @@ -1,4 +1,13 @@ -# Memory Vault v0.28.0-alpha.0.37 — automatic native mailbox receipt returns +# Memory Vault v0.28.0-alpha.0.38 — native receiver mailbox authorization + +Native TypeScript recipients can issue an original mailbox destination from +an explicitly approved contact and a registered mailbox. The receiver signs with +its own existing identity and exports bounded authorization pages for sender +admission. Python and native clients reopen the same persisted signed originals; +conflicting selections and stale revisions fail. Both clients enforce retained +parent/resource revocations before cached or fresh authorization, even after the +status expires. Authorization creates no new storage or receipt-return grants. +See `clients/typescript/network/README.md` and `docs/OPEN_NETWORK_CONTACT.md`. Native TypeScript recipients can retain an independently authorized receipt return for a cold-mailbox message. Ordinary Agent `receive` then returns the @@ -183,7 +192,7 @@ TypeScript recognizes the mailbox replica proof and status profiles. Participant operate their own authorized nodes; no central authority or project-operated public seed is required or provided. -Use the [quickstart](https://github.com/qh-work/memory-vault-sync/blob/v0.28.0-alpha.0.37/docs/OPEN_NETWORK_QUICKSTART.md) -and [mailbox replica guide](https://github.com/qh-work/memory-vault-sync/blob/v0.28.0-alpha.0.37/docs/OPEN_ACK_RECOVERY.md#receive-messages-and-shared-memories-from-a-replica-development-after-alpha025). +Use the [quickstart](https://github.com/qh-work/memory-vault-sync/blob/v0.28.0-alpha.0.38/docs/OPEN_NETWORK_QUICKSTART.md) +and [mailbox replica guide](https://github.com/qh-work/memory-vault-sync/blob/v0.28.0-alpha.0.38/docs/OPEN_ACK_RECOVERY.md#receive-messages-and-shared-memories-from-a-replica-development-after-alpha025). Preserve existing identity and state files when installing. The archives contain implementation, public documentation and wholly synthetic fixtures only. diff --git a/packaging/REVIEW_README.md b/packaging/REVIEW_README.md index 0374631..0339b45 100644 --- a/packaging/REVIEW_README.md +++ b/packaging/REVIEW_README.md @@ -1,4 +1,13 @@ -# Memory Vault v0.28.0-alpha.0.37 independent review kit +# Memory Vault v0.28.0-alpha.0.38 independent review kit + +Native TypeScript recipients can issue an original mailbox destination from +an explicitly approved contact and a registered mailbox. The receiver signs with +its own existing identity and exports bounded authorization pages for sender +admission. Python and native clients reopen the same persisted signed originals; +conflicting selections and stale revisions fail. Both clients enforce retained +parent/resource revocations before cached or fresh authorization, even after the +status expires. Authorization creates no new storage or receipt-return grants. +See `clients/typescript/network/README.md` and `docs/OPEN_NETWORK_CONTACT.md`. Native TypeScript recipients can retain an independently authorized receipt return for a cold-mailbox message. Ordinary Agent `receive` then returns the diff --git a/plugins/memory-vault-client/.codex-plugin/plugin.json b/plugins/memory-vault-client/.codex-plugin/plugin.json index fd07619..0f7caa2 100644 --- a/plugins/memory-vault-client/.codex-plugin/plugin.json +++ b/plugins/memory-vault-client/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "memory-vault-client", - "version": "0.28.0-alpha.0.37", + "version": "0.28.0-alpha.0.38", "description": "Full authorized client for the open memory protocol: MCP, visible-turn capture, queued signed sync, host adapters and recovery.", "author": { "name": "Memory Vault contributors" diff --git a/scripts/build_client_plugin.py b/scripts/build_client_plugin.py index 184be1f..47044f6 100644 --- a/scripts/build_client_plugin.py +++ b/scripts/build_client_plugin.py @@ -146,7 +146,7 @@ "clients/typescript/network/open-repair-mailbox-member.ts", "clients/typescript/network/open-repair-mailbox-feed.ts", "clients/typescript/network/open-repair-mailbox-read.ts", - "clients/typescript/network/open-repair-mailbox-inbox.ts", "clients/typescript/network/open-mailbox-client.ts", "clients/typescript/network/open-mailbox-journal.ts", "clients/typescript/network/open-mailbox-receivers.ts", "clients/typescript/network/open-mailbox-receipts.ts", + "clients/typescript/network/open-repair-mailbox-inbox.ts", "clients/typescript/network/open-mailbox-client.ts", "clients/typescript/network/open-mailbox-journal.ts", "clients/typescript/network/open-mailbox-receivers.ts", "clients/typescript/network/open-mailbox-receipts.ts", "clients/typescript/network/open-mailbox-destination.ts", "clients/typescript/network/open-repair-ack.ts", "clients/typescript/network/open-repair-probe.ts", "clients/typescript/network/open-repair-proof.ts", diff --git a/scripts/build_release.py b/scripts/build_release.py index 18887cf..66f6d2d 100644 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -123,7 +123,7 @@ "tests/test_open_repair_roundtrip.py", "tests/test_open_repair_receipt.py", "tests/test_open_repair_receipt_typescript.py", - "tests/test_open_repair_mailbox_authority_typescript.py", "tests/test_open_repair_mailbox_member_typescript.py", "tests/test_open_mailbox_client_typescript.py", "tests/test_open_mailbox_receipts_typescript.py", + "tests/test_open_repair_mailbox_authority_typescript.py", "tests/test_open_repair_mailbox_member_typescript.py", "tests/test_open_mailbox_client_typescript.py", "tests/test_open_mailbox_receipts_typescript.py", "tests/test_open_mailbox_destination_typescript.py", "tests/open_repair_index_fixtures.py", "tests/test_open_repair_stage.py", "tests/test_open_repair_index.py", @@ -248,7 +248,7 @@ "clients/typescript/network/open-repair-mailbox-member.ts", "clients/typescript/network/open-repair-mailbox-feed.ts", "clients/typescript/network/open-repair-mailbox-read.ts", - "clients/typescript/network/open-repair-mailbox-inbox.ts", "clients/typescript/network/open-mailbox-client.ts", "clients/typescript/network/open-mailbox-journal.ts", "clients/typescript/network/open-mailbox-receivers.ts", "clients/typescript/network/open-mailbox-receipts.ts", + "clients/typescript/network/open-repair-mailbox-inbox.ts", "clients/typescript/network/open-mailbox-client.ts", "clients/typescript/network/open-mailbox-journal.ts", "clients/typescript/network/open-mailbox-receivers.ts", "clients/typescript/network/open-mailbox-receipts.ts", "clients/typescript/network/open-mailbox-destination.ts", "clients/typescript/network/open-repair-ack.ts", "clients/typescript/network/open-repair-probe.ts", "clients/typescript/network/open-repair-proof.ts", diff --git a/scripts/run_open_network_ci.py b/scripts/run_open_network_ci.py index 5ef5617..894d624 100644 --- a/scripts/run_open_network_ci.py +++ b/scripts/run_open_network_ci.py @@ -54,7 +54,7 @@ "tests.test_open_repair_index_prepare", "tests.test_open_repair_index_prepare_admin", "tests.test_open_repair_provision", "tests.test_open_repair_onboarding", "tests.test_open_repair_bind_recovery", "tests.test_open_repair_remote_setup", "tests.test_open_repair_remote_provision", - "tests.test_open_repair_status_observer", "tests.test_open_repair_mailbox_resources", "tests.test_open_repair_copy_resources", "tests.test_open_repair_copy_prepare", "tests.test_open_repair_copy_state", "tests.test_open_repair_copy_service", "tests.test_open_repair_copy_upload", "tests.test_open_repair_copy_empty", "tests.test_open_repair_copy_occupied", "tests.test_open_repair_mailbox_activation", "tests.test_open_repair_mailbox_range", "tests.test_open_repair_mailbox_authority_typescript", "tests.test_open_repair_mailbox_member_typescript", "tests.test_open_mailbox_client_typescript", "tests.test_open_mailbox_receipts_typescript", "tests.test_open_repair_mailbox_root", "tests.test_open_repair_mailbox_status", "tests.test_open_repair_mailbox_source", "tests.test_open_repair_mailbox_snapshot", "tests.test_open_repair_mailbox_copy", "tests.test_open_repair_mailbox_copy_authority", "tests.test_open_repair_mailbox_copy_upload", "tests.test_open_repair_mailbox_reservation", "tests.test_open_repair_mailbox_consent", "tests.test_open_mailbox_replica_receive", "tests.test_open_mailbox_receipt_jobs", "tests.test_open_ack_replica_send", "tests.test_open_ack_typescript_agent", "tests.test_open_ack_discovery_typescript", "tests.test_open_ack_discovery_agent", "tests.test_open_mailbox_copy_jobs", "tests.test_open_repair_mailbox_copy_admin", "tests.test_open_repair_mailbox_feed_copy", "tests.test_open_repair_mailbox_message_copy", + "tests.test_open_repair_status_observer", "tests.test_open_repair_mailbox_resources", "tests.test_open_repair_copy_resources", "tests.test_open_repair_copy_prepare", "tests.test_open_repair_copy_state", "tests.test_open_repair_copy_service", "tests.test_open_repair_copy_upload", "tests.test_open_repair_copy_empty", "tests.test_open_repair_copy_occupied", "tests.test_open_repair_mailbox_activation", "tests.test_open_repair_mailbox_range", "tests.test_open_repair_mailbox_authority_typescript", "tests.test_open_repair_mailbox_member_typescript", "tests.test_open_mailbox_client_typescript", "tests.test_open_mailbox_receipts_typescript", "tests.test_open_mailbox_destination_typescript", "tests.test_open_repair_mailbox_root", "tests.test_open_repair_mailbox_status", "tests.test_open_repair_mailbox_source", "tests.test_open_repair_mailbox_snapshot", "tests.test_open_repair_mailbox_copy", "tests.test_open_repair_mailbox_copy_authority", "tests.test_open_repair_mailbox_copy_upload", "tests.test_open_repair_mailbox_reservation", "tests.test_open_repair_mailbox_consent", "tests.test_open_mailbox_replica_receive", "tests.test_open_mailbox_receipt_jobs", "tests.test_open_ack_replica_send", "tests.test_open_ack_typescript_agent", "tests.test_open_ack_discovery_typescript", "tests.test_open_ack_discovery_agent", "tests.test_open_mailbox_copy_jobs", "tests.test_open_repair_mailbox_copy_admin", "tests.test_open_repair_mailbox_feed_copy", "tests.test_open_repair_mailbox_message_copy", "tests.test_open_provider_merge", "tests.test_continuation_trial", "tests.test_network_typescript_agent_network", "tests.test_network_packaging") EXPECTED = { @@ -244,7 +244,7 @@ def initialize(reports, mode, seed, partition_index=0, partition_count=1): "open-control.ts", "open-routing.ts", "open-state.ts", "client-config.ts", "transport-state.ts", "open-contact.ts", "open-contact-state.ts", "open-contact-client.ts", "open-provider.ts", "open-provider-client.ts", "open-blob.ts", "open-repair-wire.ts", "open-repair-history.ts", "open-repair-original.ts", "open-repair-resource.ts", - "open-repair-bootstrap.ts", "open-repair-status.ts", "open-repair-mailbox-range.ts", "open-repair-mailbox-authority.ts", "open-repair-mailbox-member.ts", "open-repair-mailbox-feed.ts", "open-repair-mailbox-read.ts", "open-repair-mailbox-inbox.ts", "open-mailbox-client.ts", "open-mailbox-journal.ts", "open-mailbox-receivers.ts", "open-mailbox-receipts.ts", "open-repair-ack.ts", "open-capacity.ts", + "open-repair-bootstrap.ts", "open-repair-status.ts", "open-repair-mailbox-range.ts", "open-repair-mailbox-authority.ts", "open-repair-mailbox-member.ts", "open-repair-mailbox-feed.ts", "open-repair-mailbox-read.ts", "open-repair-mailbox-inbox.ts", "open-mailbox-client.ts", "open-mailbox-journal.ts", "open-mailbox-receivers.ts", "open-mailbox-receipts.ts", "open-mailbox-destination.ts", "open-repair-ack.ts", "open-capacity.ts", "open-repair-probe.ts", "open-repair-proof.ts", "open-repair-client.ts", "open-repair-offer-client.ts", "open-repair-receipt.ts", "open-ack-client.ts", "open-ack-status.ts", "open-ack-discovery.ts", "open-repair-admin.ts", "open-repair-bound.ts", "open-repair-empty.ts", "open-repair-occupied.ts", "open-delivery.ts", "open-delivery-control.ts", "open-delivery-client.ts", diff --git a/tests/test_network_packaging.py b/tests/test_network_packaging.py index 6a19020..b6c0564 100644 --- a/tests/test_network_packaging.py +++ b/tests/test_network_packaging.py @@ -46,7 +46,7 @@ "open-repair-original.ts", "open-repair-resource.ts", "open-repair-bootstrap.ts", - "open-repair-status.ts", "open-repair-mailbox-range.ts", "open-repair-mailbox-authority.ts", "open-repair-mailbox-member.ts", "open-repair-mailbox-feed.ts", "open-repair-mailbox-read.ts", "open-repair-mailbox-inbox.ts", "open-mailbox-client.ts", "open-mailbox-journal.ts", "open-mailbox-receivers.ts", "open-mailbox-receipts.ts", + "open-repair-status.ts", "open-repair-mailbox-range.ts", "open-repair-mailbox-authority.ts", "open-repair-mailbox-member.ts", "open-repair-mailbox-feed.ts", "open-repair-mailbox-read.ts", "open-repair-mailbox-inbox.ts", "open-mailbox-client.ts", "open-mailbox-journal.ts", "open-mailbox-receivers.ts", "open-mailbox-receipts.ts", "open-mailbox-destination.ts", "open-repair-ack.ts", "open-repair-probe.ts", "open-repair-proof.ts", "open-repair-client.ts", "open-ack-client.ts", "open-ack-status.ts", "open-ack-discovery.ts", "open-repair-admin.ts", "open-repair-bound.ts", "open-repair-empty.ts", "open-repair-occupied.ts", "open-repair-offer-client.ts", "open-repair-receipt.ts")} @@ -102,7 +102,7 @@ def test_new_sdk_docs_and_review_fixtures_are_explicit_public_paths(self): self.assertEqual(len(documents), len(set(documents))) self.assertEqual(len(review), len(set(review))) self.assertGreaterEqual(len(review), 39) - self.assertEqual(len(TS_NETWORK), 68) + self.assertEqual(len(TS_NETWORK), 69) self.assertTrue(RUNTIME_DATA <= set(documents)) self.assertTrue(TS_NETWORK <= set(documents)) self.assertTrue(TS_ENDPOINT_TESTS <= set(review)) @@ -122,7 +122,7 @@ def test_new_sdk_docs_and_review_fixtures_are_explicit_public_paths(self): "tests/test_open_repair_status.py", "tests/test_open_repair_status_typescript.py", "tests/open_repair_ack_fixtures.py", "tests/test_open_repair_ack.py", "tests/test_open_repair_ack_typescript.py", "tests/test_open_repair_state.py", "tests/test_open_repair_mailbox_resources.py", "tests/test_open_repair_copy_resources.py", - "tests/test_open_repair_copy_prepare.py", "tests/test_open_repair_copy_state.py", "tests/test_open_repair_copy_service.py", "tests/test_open_repair_copy_upload.py", "tests/test_open_repair_copy_empty.py", "tests/test_open_repair_mailbox_activation.py", "tests/test_open_repair_mailbox_range.py", "tests/test_open_repair_mailbox_authority_typescript.py", "tests/test_open_repair_mailbox_member_typescript.py", "tests/test_open_mailbox_client_typescript.py", "tests/test_open_mailbox_receipts_typescript.py", "tests/test_open_repair_mailbox_root.py", "tests/test_open_repair_mailbox_status.py", "tests/test_open_repair_mailbox_source.py", "tests/test_open_repair_mailbox_snapshot.py", "tests/test_open_repair_mailbox_copy.py", "tests/test_open_repair_mailbox_copy_authority.py", "tests/test_open_repair_mailbox_copy_upload.py", "tests/test_open_repair_mailbox_reservation.py", "tests/test_open_repair_mailbox_consent.py", "tests/test_open_mailbox_replica_receive.py", "tests/test_open_mailbox_receipt_jobs.py", "tests/test_open_ack_replica_send.py", "tests/test_open_mailbox_copy_jobs.py", "tests/test_open_repair_mailbox_copy_admin.py", "tests/test_open_repair_mailbox_feed_copy.py", "tests/test_open_repair_mailbox_message_copy.py", + "tests/test_open_repair_copy_prepare.py", "tests/test_open_repair_copy_state.py", "tests/test_open_repair_copy_service.py", "tests/test_open_repair_copy_upload.py", "tests/test_open_repair_copy_empty.py", "tests/test_open_repair_mailbox_activation.py", "tests/test_open_repair_mailbox_range.py", "tests/test_open_repair_mailbox_authority_typescript.py", "tests/test_open_repair_mailbox_member_typescript.py", "tests/test_open_mailbox_client_typescript.py", "tests/test_open_mailbox_receipts_typescript.py", "tests/test_open_mailbox_destination_typescript.py", "tests/test_open_repair_mailbox_root.py", "tests/test_open_repair_mailbox_status.py", "tests/test_open_repair_mailbox_source.py", "tests/test_open_repair_mailbox_snapshot.py", "tests/test_open_repair_mailbox_copy.py", "tests/test_open_repair_mailbox_copy_authority.py", "tests/test_open_repair_mailbox_copy_upload.py", "tests/test_open_repair_mailbox_reservation.py", "tests/test_open_repair_mailbox_consent.py", "tests/test_open_mailbox_replica_receive.py", "tests/test_open_mailbox_receipt_jobs.py", "tests/test_open_ack_replica_send.py", "tests/test_open_mailbox_copy_jobs.py", "tests/test_open_repair_mailbox_copy_admin.py", "tests/test_open_repair_mailbox_feed_copy.py", "tests/test_open_repair_mailbox_message_copy.py", "tests/test_open_repair_probe.py", "tests/test_open_repair_probe_typescript.py", "tests/test_open_repair_offer_client_typescript.py", "tests/test_open_repair_put_client_typescript.py", "tests/test_open_repair_receipt_typescript.py", @@ -188,7 +188,7 @@ def test_new_sdk_docs_and_review_fixtures_are_explicit_public_paths(self): "clients/typescript/network/open-repair-wire.ts", "clients/typescript/network/open-repair-history.ts", "clients/typescript/network/open-repair-original.ts", "clients/typescript/network/open-repair-resource.ts", "clients/typescript/network/open-repair-bootstrap.ts", "clients/typescript/network/open-repair-status.ts", - "clients/typescript/network/open-repair-mailbox-range.ts", "clients/typescript/network/open-repair-mailbox-authority.ts", "clients/typescript/network/open-repair-mailbox-member.ts", "clients/typescript/network/open-repair-mailbox-feed.ts", "clients/typescript/network/open-repair-mailbox-read.ts", "clients/typescript/network/open-repair-mailbox-inbox.ts", "clients/typescript/network/open-mailbox-client.ts", "clients/typescript/network/open-mailbox-journal.ts", "clients/typescript/network/open-mailbox-receivers.ts", "clients/typescript/network/open-mailbox-receipts.ts", + "clients/typescript/network/open-repair-mailbox-range.ts", "clients/typescript/network/open-repair-mailbox-authority.ts", "clients/typescript/network/open-repair-mailbox-member.ts", "clients/typescript/network/open-repair-mailbox-feed.ts", "clients/typescript/network/open-repair-mailbox-read.ts", "clients/typescript/network/open-repair-mailbox-inbox.ts", "clients/typescript/network/open-mailbox-client.ts", "clients/typescript/network/open-mailbox-journal.ts", "clients/typescript/network/open-mailbox-receivers.ts", "clients/typescript/network/open-mailbox-receipts.ts", "clients/typescript/network/open-mailbox-destination.ts", "clients/typescript/network/open-repair-ack.ts", "clients/typescript/network/open-capacity.ts", "clients/typescript/network/open-repair-probe.ts", "clients/typescript/network/open-repair-proof.ts", "clients/typescript/network/open-repair-client.ts", "clients/typescript/network/open-ack-client.ts", "clients/typescript/network/open-ack-status.ts", "clients/typescript/network/open-ack-discovery.ts", diff --git a/tests/test_open_mailbox_destination_typescript.py b/tests/test_open_mailbox_destination_typescript.py new file mode 100644 index 0000000..cf898f3 --- /dev/null +++ b/tests/test_open_mailbox_destination_typescript.py @@ -0,0 +1,106 @@ +"""Native recipient authority drives real sender mailbox admission over HTTP.""" +import base64 +import json +import sqlite3 +import subprocess +import unittest +import time +from pathlib import Path +from tests import test_open_delivery_http as fixture +from tests import test_open_ack_typescript_agent as runtime + +class NativeMailboxDestinationTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + runtime.NativeAckAgentTests.setUpClass.__func__(cls) + + def native(self,agent,requests): + run=subprocess.run([self.node,'--experimental-strip-types',str(self.fixture/'driver.mjs')],cwd=self.fixture, + input=json.dumps(dict(client_config=str(agent.client_config),network_config=str(agent.network_config),requests=requests,no_network=True)).encode(), + stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=45) + self.assertEqual(run.returncode,0,run.stderr.decode(errors='replace')[-2500:]) + result=json.loads(run.stdout);self.assertEqual(result['calls'],[]);self.assertEqual(result['subprocessCalls'],0) + state=Path(json.loads(Path(agent.network_config).read_bytes())['state_directory'])/'network.sqlite3' + db=sqlite3.connect(state) + try:self.assertEqual(db.execute('PRAGMA integrity_check').fetchall(),[('ok',)]) + finally:db.close() + return result['results'] + + def test_native_authorizes_real_admission_and_python_reopens_exact_pages(self): + self.journey(False) + + def test_native_authorization_rejects_conflicts_expiry_and_changed_receiver(self): + self.journey(True) + + def test_expired_retained_revocation_blocks_cached_and_new_authorization(self): + self.journey(False,True) + + def journey(self,negative,revoked=False): + h=fixture.MailboxStagingHTTPTests('test_sender_admits_message_over_http');h.setUp();self.addCleanup(h.doCleanups) + original=h.call;visited=[];pages=[];first=[] + def dispatch(agent,**request): + invitation=request.get('invitation',{}) + if agent is not h.b or invitation.get('schema_version')!='memory-vault-open-mailbox-connect/v1' or invitation.get('action') not in ('register','authorize'): + return original(agent,**request) + rows=self.native(agent,[request]);self.assertTrue(rows[0]['ok'],rows[0]);result=rows[0]['result'] + if invitation['action']=='register': + self.assertEqual(result,original(agent,**request)) + inspected=self.native(agent,[dict(op='connect',invitation=dict(schema_version=invitation['schema_version'],action='inspect',receiver_id=result['receiver_id']))])[0] + self.assertTrue(inspected['ok'],inspected);self.assertEqual(inspected['result'],original(agent,op='connect',invitation=dict(schema_version=invitation['schema_version'],action='inspect',receiver_id=result['receiver_id']))) + return result + # Each page reopens in a new native process, then Python reads the same exact originals. + self.assertEqual(result,original(agent,**request));pages.append(base64.b64decode(result['authorization_chunk'],validate=True)) + if 'cursor' not in invitation: + visited.append(True);first.append(dict(invitation)) + if negative: + cases=[(dict(invitation,expires_at=invitation['expires_at']-1),'repair_destination_conflict'), + (dict(invitation,status_revision=1),'repair_destination_revision_rollback'), + (dict(invitation,status_revision=4,expires_at=1),'repair_resource_expired'), + (dict(invitation,status_revision=4,status_until=1),'repair_resource_expired'), + (dict(invitation,cursor=dict(sha256='0'*64,offset=3072)),'open_invalid_mailbox_cursor'), + (dict(invitation,receiver_id='mailbox_'+'0'*64),'open_mailbox_receiver_missing')] + for changed,code in cases: + denied=self.native(agent,[dict(op='connect',invitation=changed)])[0] + self.assertFalse(denied['ok'],denied);self.assertEqual(denied['error']['code'],code,denied) + state=Path(json.loads(Path(agent.network_config).read_bytes())['state_directory'])/'network.sqlite3' + db=sqlite3.connect(state) + try: + self.assertEqual(db.execute('SELECT revision FROM open_mailbox_destinations ORDER BY revision').fetchall(),[(2,),(3,)]) + row=db.execute('SELECT body FROM open_mailbox_receivers WHERE receiver_id=?',(invitation['receiver_id'],)).fetchone() + config=json.loads(row[0]);config['expected_sender']=config['expected_target'] + db.execute('UPDATE open_mailbox_receivers SET body=? WHERE receiver_id=?',(json.dumps(config).encode(),invitation['receiver_id']));db.commit() + finally:db.close() + wrong=self.native(agent,[request])[0];self.assertFalse(wrong['ok']);self.assertEqual(wrong['error']['code'],'open_invalid_mailbox_receiver') + db=sqlite3.connect(state) + try: + db.execute('UPDATE open_mailbox_receivers SET body=? WHERE receiver_id=?',(row[0],invitation['receiver_id']));db.commit() + finally:db.close() + self.assertEqual(self.native(agent,[request])[0]['result'],result) + return result + h.call=dispatch;h.test_sender_admits_message_over_http();self.assertEqual(visited,[True]);self.assertGreater(len(pages),1) + authorization=json.loads(b''.join(pages));self.assertEqual(authorization,h.sender_authorization) + # The fixture performs actual admission, feed/body recovery and authority denial checks. + self.assertIn('destination_entry',authorization);self.assertIn('owner_status_entry',authorization) + if revoked: + from memory_vault_open_provider import issue_status + from memory_vault_open_repair_status import status_scope,authenticate_status_original + from memory_vault_open_repair_state import DEFAULT_POLICY + from memory_vault_open_repair_wire import RepairBudget + from memory_vault_open_repair_client import MailboxSetupJournal + from tests.test_open_repair_status import status_entry + slot=json.loads(authorization['slot_entries']['slot']['raw'])['payload'];root=slot['slot_key']['root_key'];now=int(time.time()) + scope=dict(scope_kind='mailbox_slot',scope_id=status_scope(root,'mailbox_slot',slot['slot_key'],DEFAULT_POLICY,RepairBudget(DEFAULT_POLICY))) + entry=status_entry(issue_status(h.bi,root=root,revision=99,entries=[dict(scope,minimum_document_revision=slot['revision'],status='revoked',operation_mask=127)],issued_at=now-10,valid_until=now-1)) + observed=authenticate_status_original(entry,expected_root=root,expected_signing_key=h.bi.public_descriptor(),at=now-10,allowed_scopes=[scope],policy=DEFAULT_POLICY,budget=RepairBudget(DEFAULT_POLICY)) + with h.b._network() as network: + with network.participant.state.db() as db: + jobs=db.execute('SELECT job_key FROM open_mailbox_setup_jobs').fetchall();self.assertEqual(len(jobs),1) + MailboxSetupJournal(db).observe(jobs[0][0],observed) + for revision in (3,100): + denied=self.native(h.b,[dict(op='connect',invitation=dict(first[0],status_revision=revision))])[0] + self.assertFalse(denied['ok']);self.assertEqual(denied['error']['code'],'repair_authority_revoked',denied) + reopened=h.b.handle(dict(op='connect',invitation=dict(first[0],status_revision=revision))) + self.assertFalse(reopened['ok']);self.assertEqual(reopened['error']['code'],'repair_authority_revoked',reopened) + + +if __name__=='__main__':unittest.main()