@kehto/runtime 0.25.1 → 0.26.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 +67 -0
- package/dist/index.d.ts +39 -4
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,68 @@ The runtime is built around the current draft dispatch contract from `@napplet/c
|
|
|
40
40
|
|
|
41
41
|
Signing is shell-mediated inside `relay.publish` / `relay.publishEncrypted` (NIP-44 default, NIP-04 opt-in). The legacy signer domain is dissolved — napplets never see a host-injected nostr object and cannot call signer-sign RPCs directly.
|
|
42
42
|
|
|
43
|
+
### Storage quota administration
|
|
44
|
+
|
|
45
|
+
Hosts can call `runtime.aclState.setStateQuota(pubkey, dTag, aggregateHash, bytes)`
|
|
46
|
+
to update the exact principal's live storage limit. `bytes` must be an integer
|
|
47
|
+
from zero through `Number.MAX_SAFE_INTEGER`; malformed input throws `RangeError`
|
|
48
|
+
before changing policy. Call `aclState.persist()` separately to save the quota.
|
|
49
|
+
The default remains 524,288 bytes. Grants and block status are preserved.
|
|
50
|
+
|
|
51
|
+
The existing storage handler consumes this same quota across shared and instance
|
|
52
|
+
storage for that principal, counting UTF-8 bytes including scoped storage keys.
|
|
53
|
+
Lowering a quota preserves stored values; writes that exceed the new limit fail
|
|
54
|
+
until data is removed or the host increases the quota. Zero prevents nonempty
|
|
55
|
+
storage writes while reads/removals remain subject to their existing ACL checks.
|
|
56
|
+
|
|
57
|
+
This host administration API conforms to [NAP-STORAGE draft PR #3 at
|
|
58
|
+
`4f4cf0ca7ecbd24c284a5d63bcd85e3de6af37ca`](https://github.com/napplet/naps/blob/4f4cf0ca7ecbd24c284a5d63bcd85e3de6af37ca/naps/NAP-STORAGE.md).
|
|
59
|
+
It adds no napplet-callable quota setter or wire field.
|
|
60
|
+
|
|
61
|
+
### ACL principal enumeration
|
|
62
|
+
|
|
63
|
+
`aclState.getAllEntries()` and `getEntry()` return `dTag` and `aggregateHash`
|
|
64
|
+
alongside the policy fields. Use these values with keyed ACL methods; two
|
|
65
|
+
principals with identical grants still have distinct identities after reload.
|
|
66
|
+
Enumeration keeps `pubkey: ''` because ACL identity is keyed by dTag and artifact
|
|
67
|
+
hash. Returned objects and capability arrays are detached from the owned state.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
for (const entry of runtime.aclState.getAllEntries()) {
|
|
71
|
+
runtime.aclState.revoke(entry.pubkey, entry.dTag, entry.aggregateHash, 'relay:read');
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This host API adds no protocol messages and does not change serialized ACL keys.
|
|
76
|
+
Custom `AclEntryExternal` producers must populate the two required identity fields.
|
|
77
|
+
|
|
78
|
+
### ACL persistence outcomes
|
|
79
|
+
|
|
80
|
+
ACL mutations update live policy immediately. Revoke/block observers run
|
|
81
|
+
synchronously after that change, before any explicit save. `aclState.persist()`
|
|
82
|
+
returns `true` when the persistence backend completes and `false` if it throws.
|
|
83
|
+
A failed save never rolls back live policy or reactivates retired subscriptions;
|
|
84
|
+
a newly loaded container can still see the last successfully saved policy.
|
|
85
|
+
|
|
86
|
+
`aclState.clear()` resets live policy to its configured default and attempts to
|
|
87
|
+
clear storage, returning the same success boolean. A failed clear leaves that
|
|
88
|
+
live reset in place and old durable data unchanged. Retry persistence or clearing
|
|
89
|
+
after handling the backend failure. These results report the synchronous backend
|
|
90
|
+
contract, not physical disk durability. Hosts must not swallow backend errors if
|
|
91
|
+
they want the container to detect failure.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
runtime.aclState.revoke('', dTag, aggregateHash, 'relay:read');
|
|
95
|
+
if (!runtime.aclState.persist()) {
|
|
96
|
+
showSaveError('Access is revoked for this session, but the policy was not saved.');
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Runtime destruction remains best-effort; hosts requiring an actionable save
|
|
101
|
+
result should call `persist()` before teardown. This host API policy is outside
|
|
102
|
+
the NAP wire contract. No old-use warning can detect callers ignoring a return
|
|
103
|
+
value; see [the compatibility register](https://github.com/kehto/web/blob/main/docs/compatibility.md).
|
|
104
|
+
|
|
43
105
|
### NAP-RELAY subscription revocation
|
|
44
106
|
|
|
45
107
|
Revoking `relay:read` or blocking a principal immediately retires ordinary relay
|
|
@@ -205,6 +267,11 @@ runtime.handleMessage('window-1', {
|
|
|
205
267
|
- `createSessionRegistry` — bidirectional windowId ↔ `SessionEntry` store
|
|
206
268
|
- `createNappKeyRegistry` — deprecated alias retained for v1.1 migration consumers
|
|
207
269
|
|
|
270
|
+
`unregister(windowId)` retires the window's registration, lookup state, and pending
|
|
271
|
+
update, including NIP-5D sessions whose pubkey is empty. Repeated calls are safe.
|
|
272
|
+
If a newer window owns the same pubkey lookup, that newer entry is preserved;
|
|
273
|
+
use `getEntryByWindowId` to distinguish live windows sharing an empty pubkey.
|
|
274
|
+
|
|
208
275
|
`runtime.sessionRegistry.register(windowId, entry)` establishes a fresh trusted
|
|
209
276
|
source lifecycle and retires that window's previous initialization burst budget,
|
|
210
277
|
including when the host reuses the window ID, timestamp, or entry object. Ordinary
|
package/dist/index.d.ts
CHANGED
|
@@ -530,6 +530,10 @@ type PendingUpdateNotifier = (windowId: string) => void;
|
|
|
530
530
|
/** External ACL entry — used in shell commands (shell:acl-get etc.). */
|
|
531
531
|
interface AclEntryExternal {
|
|
532
532
|
pubkey: string;
|
|
533
|
+
/** Verified napplet identifier. */
|
|
534
|
+
dTag: string;
|
|
535
|
+
/** Verified artifact hash. */
|
|
536
|
+
aggregateHash: string;
|
|
533
537
|
capabilities: Capability[];
|
|
534
538
|
blocked: boolean;
|
|
535
539
|
stateQuota?: number;
|
|
@@ -959,7 +963,7 @@ declare function formatDenialReason(capability: Capability): string;
|
|
|
959
963
|
interface SessionRegistry {
|
|
960
964
|
/** Establish a fresh host-attested lifecycle, mapping windowId to pubkey and vice versa. */
|
|
961
965
|
register(windowId: string, entry: SessionEntry): void;
|
|
962
|
-
/**
|
|
966
|
+
/** Remove a window and its owned mappings, including empty-pubkey sessions. */
|
|
963
967
|
unregister(windowId: string): void;
|
|
964
968
|
/** Get the pubkey associated with a windowId. */
|
|
965
969
|
getPubkey(windowId: string): string | undefined;
|
|
@@ -1037,7 +1041,7 @@ type AclMutation = {
|
|
|
1037
1041
|
readonly dTag: string;
|
|
1038
1042
|
readonly aggregateHash: string;
|
|
1039
1043
|
};
|
|
1040
|
-
/** Receives synchronous
|
|
1044
|
+
/** Receives synchronous in-memory revoke/block changes, before any explicit persistence. */
|
|
1041
1045
|
type AclMutationObserver = (mutation: AclMutation) => void;
|
|
1042
1046
|
/**
|
|
1043
1047
|
* ACL state container — wraps @kehto/acl's pure functions with
|
|
@@ -1060,9 +1064,40 @@ interface AclStateContainer {
|
|
|
1060
1064
|
getEntry(pubkey: string, dTag: string, aggregateHash: string): AclEntryExternal | undefined;
|
|
1061
1065
|
getAllEntries(): AclEntryExternal[];
|
|
1062
1066
|
getStateQuota(pubkey: string, dTag: string, aggregateHash: string): number;
|
|
1063
|
-
|
|
1067
|
+
/**
|
|
1068
|
+
* Set a principal's live storage quota without changing grants or stored data.
|
|
1069
|
+
* Call persist() separately to save the policy.
|
|
1070
|
+
* @param pubkey - Retained identity argument; policy is keyed by dTag and hash.
|
|
1071
|
+
* @param dTag - Verified napplet identifier.
|
|
1072
|
+
* @param aggregateHash - Verified artifact hash.
|
|
1073
|
+
* @param bytes - Integer byte count from zero through Number.MAX_SAFE_INTEGER.
|
|
1074
|
+
* @throws RangeError if bytes is not a non-negative safe integer.
|
|
1075
|
+
* @example
|
|
1076
|
+
* ```ts
|
|
1077
|
+
* acl.setStateQuota('', dTag, aggregateHash, 1024 * 1024);
|
|
1078
|
+
* acl.persist();
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
setStateQuota(pubkey: string, dTag: string, aggregateHash: string, bytes: number): void;
|
|
1082
|
+
/**
|
|
1083
|
+
* Save live state without rolling back live policy on failure.
|
|
1084
|
+
* @returns True if the backend completed; false if it threw.
|
|
1085
|
+
* @example
|
|
1086
|
+
* ```ts
|
|
1087
|
+
* if (!acl.persist()) showSaveError();
|
|
1088
|
+
* ```
|
|
1089
|
+
*/
|
|
1090
|
+
persist(): boolean;
|
|
1064
1091
|
load(): void;
|
|
1065
|
-
|
|
1092
|
+
/**
|
|
1093
|
+
* Reset live state to the configured default and attempt to clear storage.
|
|
1094
|
+
* @returns True if storage cleared; false if only the live reset succeeded.
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```ts
|
|
1097
|
+
* if (!acl.clear()) showSaveError();
|
|
1098
|
+
* ```
|
|
1099
|
+
*/
|
|
1100
|
+
clear(): boolean;
|
|
1066
1101
|
}
|
|
1067
1102
|
/**
|
|
1068
1103
|
* Create an ACL state container backed by @kehto/acl and persisted
|
package/dist/index.js
CHANGED
|
@@ -51,10 +51,10 @@ function createSessionRegistry(notifier, onRegister) {
|
|
|
51
51
|
},
|
|
52
52
|
unregister(windowId) {
|
|
53
53
|
const pubkey = byWindowId.get(windowId);
|
|
54
|
-
if (pubkey) {
|
|
54
|
+
if (pubkey !== void 0 && byPubkey.get(pubkey)?.windowId === windowId) {
|
|
55
55
|
byPubkey.delete(pubkey);
|
|
56
|
-
byWindowId.delete(windowId);
|
|
57
56
|
}
|
|
57
|
+
byWindowId.delete(windowId);
|
|
58
58
|
byWindowIdEntry.delete(windowId);
|
|
59
59
|
pendingUpdates.delete(windowId);
|
|
60
60
|
},
|
|
@@ -122,6 +122,7 @@ import {
|
|
|
122
122
|
serialize,
|
|
123
123
|
deserialize,
|
|
124
124
|
getQuota,
|
|
125
|
+
setQuota,
|
|
125
126
|
CAP_RELAY_READ,
|
|
126
127
|
CAP_RELAY_WRITE,
|
|
127
128
|
CAP_CACHE_READ,
|
|
@@ -237,15 +238,20 @@ function createAclState(persistence, defaultPolicy = "permissive", onMutation) {
|
|
|
237
238
|
if (!entry) return void 0;
|
|
238
239
|
return {
|
|
239
240
|
pubkey: pubkey || "",
|
|
241
|
+
dTag,
|
|
242
|
+
aggregateHash,
|
|
240
243
|
capabilities: bitsToCapabilities(entry.caps),
|
|
241
244
|
blocked: entry.blocked,
|
|
242
245
|
stateQuota: entry.quota
|
|
243
246
|
};
|
|
244
247
|
},
|
|
245
248
|
getAllEntries() {
|
|
246
|
-
return Object.entries(state.entries).map(([, entry]) => {
|
|
249
|
+
return Object.entries(state.entries).map(([key, entry]) => {
|
|
250
|
+
const separator = key.lastIndexOf(":");
|
|
247
251
|
return {
|
|
248
252
|
pubkey: "",
|
|
253
|
+
dTag: key.slice(0, separator),
|
|
254
|
+
aggregateHash: key.slice(separator + 1),
|
|
249
255
|
capabilities: bitsToCapabilities(entry.caps),
|
|
250
256
|
blocked: entry.blocked,
|
|
251
257
|
stateQuota: entry.quota
|
|
@@ -256,10 +262,20 @@ function createAclState(persistence, defaultPolicy = "permissive", onMutation) {
|
|
|
256
262
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
257
263
|
return getQuota(state, id);
|
|
258
264
|
},
|
|
265
|
+
setStateQuota(pubkey, dTag, aggregateHash, bytes) {
|
|
266
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0) {
|
|
267
|
+
throw new RangeError("State quota must be a non-negative safe integer byte count");
|
|
268
|
+
}
|
|
269
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
270
|
+
ensureRuntimeDefaultEntry(id);
|
|
271
|
+
state = setQuota(state, id, bytes);
|
|
272
|
+
},
|
|
259
273
|
persist() {
|
|
260
274
|
try {
|
|
261
275
|
persistence.persist(serialize(state));
|
|
276
|
+
return true;
|
|
262
277
|
} catch {
|
|
278
|
+
return false;
|
|
263
279
|
}
|
|
264
280
|
},
|
|
265
281
|
load() {
|
|
@@ -275,7 +291,9 @@ function createAclState(persistence, defaultPolicy = "permissive", onMutation) {
|
|
|
275
291
|
state = createState(defaultPolicy);
|
|
276
292
|
try {
|
|
277
293
|
persistence.persist("");
|
|
294
|
+
return true;
|
|
278
295
|
} catch {
|
|
296
|
+
return false;
|
|
279
297
|
}
|
|
280
298
|
}
|
|
281
299
|
};
|