Cover digital assets and vehicles with proper fast-path instruments

Digital assets: nonprobate.js's suggestion previously claimed channel
'bank-mandate' with label 'Multi-sig / pre-authorized release' — a
mismatch, since bank-mandate's fields (institution/nominee/reference)
don't fit crypto custody at all. New "digital-custody" channel with
custody-type-specific fields: exchange beneficiary feature, self-custody
multi-sig (warns that it only works if a living co-signer is already
configured), or key-escrow with executor (warns explicitly: never store
the actual seed phrase or private key in this app or any single record —
only a reference to WHERE access is arranged).

Vehicles/jewelry/valuables: were lumped into the same "trust" suggestion
as land, which is overkill for movable property that hands over cleanly
by a simple Hibah. Suggestion now points straight to Hibah for these;
land keeps the trust suggestion since it has no such easy path.

CoverageDashboard's suggestion-inline text simplified from an ad-hoc
ternary chain into a clean NEXT_STEP_TAB map, now correctly covering
hibah/trust/digital-custody/business-continuity.

e2e-digital-vehicle.cjs: new suite verifying digital asset exposed ->
custody-type warnings -> export -> covered, and vehicle exposed -> Hibah
suggestion (not trust) -> covered via Hibah link -> 100% combined
coverage. 10/10 passing. Re-verified all four prior suites (32/32,
16/16, 12/12, 10/10) — 80/80 total, no regressions.
This commit is contained in:
wmj
2026-08-13 17:41:25 +08:00
parent 051c9d9a94
commit 07b9f4f07a
5 changed files with 192 additions and 7 deletions
+62 -4
View File
@@ -20,7 +20,14 @@
{ value: 'takaful', label: 'Takaful / insurance nomination', note: 'Nomination as trust under Insurance Act 1996 s.166 (or Takaful equivalent) — pays the named beneficiary directly.' },
{ value: 'bank-mandate', label: 'Bank death mandate / joint account', note: 'Ask the bank for a death-benefit nomination, or hold as joint account with survivorship.' },
{ value: 'trust', label: 'Pre-funded trust / nominee holding', note: 'For land or illiquid assets with no nomination bypass — requires setting up a real trust or nominee entity now, while alive.' },
{ value: 'business-continuity', label: 'Business continuity instrument', note: 'A business dies with its structure by default: a sole proprietorship dies with the owner, a partnership dissolves on any partner\'s death unless the agreement says otherwise, and company shares fall into probate like any other asset unless a buy-sell or shareholder agreement already routes them.' }
{ value: 'business-continuity', label: 'Business continuity instrument', note: 'A business dies with its structure by default: a sole proprietorship dies with the owner, a partnership dissolves on any partner\'s death unless the agreement says otherwise, and company shares fall into probate like any other asset unless a buy-sell or shareholder agreement already routes them.' },
{ value: 'digital-custody', label: 'Digital custody plan', note: 'A single seed phrase known only to you is not a fast path — it is a total-loss risk if you die without sharing it. Use the exchange\'s own beneficiary feature where one exists, or a multi-sig / key-escrow arrangement with your executor for self-custody wallets.' }
];
const CUSTODY_TYPES = [
{ value: 'exchange-beneficiary', label: 'Custodial exchange beneficiary feature', warning: null },
{ value: 'multisig', label: 'Self-custody multi-sig wallet', warning: 'Multi-sig only works if the co-signers are set up and able to act now — an executor who is a co-signer can release funds without waiting for probate, but only if the wallet was actually configured this way while you were alive.' },
{ value: 'key-escrow', label: 'Key-escrow with executor', warning: 'Never store the actual seed phrase or private key in this app or any single record your executor can\'t independently verify. This field should reference WHERE the key is escrowed (e.g. a sealed instruction with a lawyer, a hardware device with split-knowledge access) — not the key itself.' }
];
const BUSINESS_STRUCTURES = [
@@ -34,7 +41,7 @@
{ value: 'waqf-enterprise', label: 'Dedicate as waqf-owned enterprise', note: 'Use the Family Waqf Designator for this instead — a waqf-owned enterprise survives you and serves your purpose in perpetuity, same as any other waqf corpus.' }
];
const emptyForm = () => ({ linkedAssetId: '', type: 'epf', institution: '', nomineeeName: '', referenceNumber: '', trusteeName: '', successorTrustee: '', trustBeneficiaries: '', businessStructure: 'company', businessInstrument: 'shareholder-buy-sell', successorOwner: '', buySellTerms: '' });
const emptyForm = () => ({ linkedAssetId: '', type: 'epf', institution: '', nomineeeName: '', referenceNumber: '', trusteeName: '', successorTrustee: '', trustBeneficiaries: '', businessStructure: 'company', businessInstrument: 'shareholder-buy-sell', successorOwner: '', buySellTerms: '', custodyType: 'exchange-beneficiary', platform: '', keyHolderName: '', accessInstructionsRef: '' });
let nominations = $state(load('nominations', []));
let form = $state(emptyForm());
@@ -46,6 +53,8 @@
} else if (form.type === 'business-continuity') {
if (form.businessInstrument === 'waqf-enterprise') return; // redirect only, nothing to save here
if (!form.successorOwner) return;
} else if (form.type === 'digital-custody') {
if (!form.keyHolderName) return;
} else if (!form.nomineeeName) {
return;
}
@@ -85,6 +94,37 @@
URL.revokeObjectURL(url);
}
function exportDigitalCustody(n) {
const asset = assets.find(a => a.id === n.linkedAssetId);
const custodyLabel = CUSTODY_TYPES.find(c => c.value === n.custodyType)?.label;
const lines = [
'DIGITAL CUSTODY PLAN — DRAFTING AID',
`Generated: ${new Date().toISOString().slice(0, 10)}`,
`Asset: ${asset ? asset.description : '(asset removed)'}`,
`Platform / wallet: ${n.platform || '________________________'}`,
`Custody type: ${custodyLabel}`,
`Key holder / executor: ${n.keyHolderName}`,
`Access instructions reference: ${n.accessInstructionsRef || '________________________'}`,
'',
n.custodyType === 'exchange-beneficiary'
? 'ACTION: File the death claim directly with the exchange/custodial platform using\nits own beneficiary/inheritance feature. This is a platform process, not a\nprobate filing — confirm the feature is actually activated on the account now,\nwhile you\'re alive, not assumed to exist.'
: n.custodyType === 'multisig'
? 'ACTION: The named co-signer(s) release funds per the wallet\'s multi-sig\nconfiguration once death is confirmed. This only works if the multi-sig was\nactually set up with a living co-signer — a single-key wallet has no fast path\nat all.'
: 'ACTION: Retrieve the escrowed access instructions from wherever they are held\n(named above) and follow them to access the wallet. This app does not, and must\nnot, store the seed phrase or private key itself.',
'',
'WARNING: never enter an actual seed phrase, private key, or password into this',
'app or any single document. This drafting aid should only ever reference WHERE',
'access is arranged, never the credential itself.',
'',
'DISCLAIMER: Not a fatwa. Not legal advice. Drafting aid only.'
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'digital-custody-plan-draft.txt'; a.click();
URL.revokeObjectURL(url);
}
function exportTrustDeed(n) {
const asset = assets.find(a => a.id === n.linkedAssetId);
const lines = [
@@ -178,12 +218,26 @@
<label class="field"><span>Terms</span><input type="text" bind:value={form.buySellTerms} placeholder="e.g. valuation method, Takaful-funded buyout" /></label>
{/if}
</div>
{:else if form.type === 'digital-custody'}
<div class="trust-fields">
<label class="field"><span>Platform / wallet</span><input type="text" bind:value={form.platform} placeholder="e.g. Coinbase, Ledger self-custody" /></label>
<label class="field"><span>Custody type</span>
<select bind:value={form.custodyType}>
{#each CUSTODY_TYPES as c}<option value={c.value}>{c.label}</option>{/each}
</select>
</label>
{#if CUSTODY_TYPES.find(c => c.value === form.custodyType)?.warning}
<p class="business-warning">{CUSTODY_TYPES.find(c => c.value === form.custodyType).warning}</p>
{/if}
<label class="field"><span>Key holder / executor</span><input type="text" bind:value={form.keyHolderName} /></label>
<label class="field"><span>Access instructions reference (never the actual key)</span><input type="text" bind:value={form.accessInstructionsRef} placeholder="e.g. sealed letter with lawyer X, ref #123" /></label>
</div>
{:else}
<label class="field"><span>Institution</span><input type="text" bind:value={form.institution} placeholder="e.g. KWSP, Bank Islam, Takaful Ikhlas" /></label>
<label class="field"><span>Nominee name</span><input type="text" bind:value={form.nomineeeName} /></label>
<label class="field"><span>Reference / policy / account number</span><input type="text" bind:value={form.referenceNumber} /></label>
{/if}
<button class="btn-primary" disabled={form.type === 'business-continuity' && form.businessInstrument === 'waqf-enterprise'} onclick={addNomination}>Add {form.type === 'trust' ? 'trust setup' : form.type === 'business-continuity' ? 'business continuity instrument' : 'nomination'}</button>
<button class="btn-primary" disabled={form.type === 'business-continuity' && form.businessInstrument === 'waqf-enterprise'} onclick={addNomination}>Add {form.type === 'trust' ? 'trust setup' : form.type === 'business-continuity' ? 'business continuity instrument' : form.type === 'digital-custody' ? 'digital custody plan' : 'nomination'}</button>
</div>
{#each nominations as n (n.id)}
@@ -192,13 +246,16 @@
<div class="nomination-row">
<div class="nomination-info">
<strong>{asset ? asset.description : '(asset removed)'}</strong>
<span class="muted">{channelInfo?.label}{n.type !== 'trust' && n.type !== 'business-continuity' ? ` · ${n.institution || '—'}` : ''}</span>
<span class="muted">{channelInfo?.label}{!['trust', 'business-continuity', 'digital-custody'].includes(n.type) ? ` · ${n.institution || '—'}` : ''}</span>
{#if n.type === 'trust'}
<span class="muted">Trustee: {n.trusteeName} · Successor: {n.successorTrustee || '—'}</span>
<span class="muted">Beneficiaries: {n.trustBeneficiaries || '—'}</span>
{:else if n.type === 'business-continuity'}
<span class="muted">{BUSINESS_INSTRUMENTS.find(i => i.value === n.businessInstrument)?.label}</span>
<span class="muted">Successor: {n.successorOwner} · Terms: {n.buySellTerms || '—'}</span>
{:else if n.type === 'digital-custody'}
<span class="muted">{CUSTODY_TYPES.find(c => c.value === n.custodyType)?.label} · {n.platform || '—'}</span>
<span class="muted">Key holder: {n.keyHolderName}</span>
{:else}
<span class="muted">Nominee: {n.nomineeeName} · Ref: {n.referenceNumber || '—'}</span>
{/if}
@@ -206,6 +263,7 @@
<div class="row-actions">
{#if n.type === 'trust'}<button class="export-btn" onclick={() => exportTrustDeed(n)}>Export</button>{/if}
{#if n.type === 'business-continuity'}<button class="export-btn" onclick={() => exportBusinessContinuity(n)}>Export</button>{/if}
{#if n.type === 'digital-custody'}<button class="export-btn" onclick={() => exportDigitalCustody(n)}>Export</button>{/if}
<button onclick={() => remove(n.id)}>✕</button>
</div>
</div>