Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
36d006a
Read independent ACK sources with the native receipt writer
qh-work Sep 30, 2026
6d36f3f
Upload native saved receipts and retain interoperable recovery journals
qh-work Sep 30, 2026
83011b9
Stop ACK upload when current permission expires during journal persis…
qh-work Sep 30, 2026
5c2594d
Replay native receipt journals across restarts without renewing autho…
qh-work Sep 30, 2026
e383f65
Return actual saved receipts through the native Agent and shared dura…
qh-work Sep 30, 2026
2479cf1
Align alpha35 plugin version and include native writer review fixtures
qh-work Sep 30, 2026
751978a
Authenticate native mailbox owner controls and persisted resource act…
qh-work Sep 30, 2026
21d59cb
Authenticate native mailbox admission and complete retained feed history
qh-work Sep 30, 2026
d86d620
Keep exact public SDK inventory aligned with native receipt return
qh-work Sep 30, 2026
08626c6
Resume retained mailbox memory imports and saved receipts in native A…
qh-work Sep 30, 2026
18cfccf
Merge branch 'feat/native-ack-offer' into feat/native-mailbox-read
qh-work Sep 30, 2026
c052c5a
Receive registered remote mailbox memories in native Agent
qh-work Sep 30, 2026
325c867
Merge published alpha35 baseline into native mailbox recovery
qh-work Sep 30, 2026
13fcdcf
Merge branch 'main' of https://github.com/qh-work/memory-vault-sync i…
qh-work Sep 30, 2026
0614aa4
Prepare alpha36 native remote mailbox recovery candidate
qh-work Sep 30, 2026
5945c9a
Bound the expanded native mailbox release inventory at 640 files
qh-work Sep 30, 2026
bd3ca97
Align alpha36 plugin and installation guides with mailbox recovery
qh-work Sep 30, 2026
b82d66b
Return retained cold mailbox receipts during native Agent receive
qh-work Sep 30, 2026
4734a1c
Merge published alpha36 baseline preserving native receipt returns
qh-work Sep 30, 2026
0b6b1c4
Prepare alpha37 automatic native mailbox receipt release
qh-work Sep 30, 2026
fe943c6
Merge branch 'main' of https://github.com/qh-work/memory-vault-sync i…
qh-work Sep 30, 2026
d7d17b2
Authorize native mailbox admission with retained original permissions
qh-work Sep 30, 2026
531f9ed
Prepare alpha38 native receiver authorization release
qh-work Sep 30, 2026
14349e3
Merge published alpha37 baseline preserving receiver authorization
qh-work Sep 30, 2026
960840f
Merge branch 'main' of https://github.com/qh-work/memory-vault-sync i…
qh-work Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions clients/typescript/network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 7 additions & 0 deletions clients/typescript/network/open-contact-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,()=>{
Expand Down
99 changes: 99 additions & 0 deletions clients/typescript/network/open-mailbox-destination.ts
Original file line number Diff line number Diff line change
@@ -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<string,any>;
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<string,{revision:number;mask:number}>();
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<string,string>();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(!(at<value.expires_at&&value.expires_at<=Math.min(...['slot','read','maintenance'].map(n=>setup[n].payload.expires_at as number),held.originals.grant.payload.expires_at as number))
||!(at<value.status_until&&value.status_until<=Math.min(...Object.values(setup).map(e=>e.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_kind<b.scope_kind?-1:a.scope_kind>b.scope_kind?1:a.scope_id<b.scope_id?-1:a.scope_id>b.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'};
}
16 changes: 16 additions & 0 deletions clients/typescript/network/open-mailbox-receivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,any>;
export const MAILBOX_CONNECT_SCHEMA='memory-vault-open-mailbox-connect/v1';
Expand Down Expand Up @@ -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;}
Expand Down
3 changes: 2 additions & 1 deletion clients/typescript/network/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading