@kehto/runtime 0.25.2 → 0.26.1
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 +62 -0
- package/dist/index.d.ts +40 -3
- package/dist/index.js +28 -6
- 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
|
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;
|
|
@@ -633,6 +637,8 @@ interface ServiceHandler {
|
|
|
633
637
|
* @param windowId - The destroyed napplet's window identifier
|
|
634
638
|
*/
|
|
635
639
|
onWindowDestroyed?(windowId: string): void;
|
|
640
|
+
/** Revoke live output when this endpoint loses a capability or is blocked. */
|
|
641
|
+
onCapabilityRevoked?(windowId: string, capability: Capability | null): void;
|
|
636
642
|
}
|
|
637
643
|
/**
|
|
638
644
|
* Registry of services available to napplets.
|
|
@@ -1037,7 +1043,7 @@ type AclMutation = {
|
|
|
1037
1043
|
readonly dTag: string;
|
|
1038
1044
|
readonly aggregateHash: string;
|
|
1039
1045
|
};
|
|
1040
|
-
/** Receives synchronous
|
|
1046
|
+
/** Receives synchronous in-memory revoke/block changes, before any explicit persistence. */
|
|
1041
1047
|
type AclMutationObserver = (mutation: AclMutation) => void;
|
|
1042
1048
|
/**
|
|
1043
1049
|
* ACL state container — wraps @kehto/acl's pure functions with
|
|
@@ -1060,9 +1066,40 @@ interface AclStateContainer {
|
|
|
1060
1066
|
getEntry(pubkey: string, dTag: string, aggregateHash: string): AclEntryExternal | undefined;
|
|
1061
1067
|
getAllEntries(): AclEntryExternal[];
|
|
1062
1068
|
getStateQuota(pubkey: string, dTag: string, aggregateHash: string): number;
|
|
1063
|
-
|
|
1069
|
+
/**
|
|
1070
|
+
* Set a principal's live storage quota without changing grants or stored data.
|
|
1071
|
+
* Call persist() separately to save the policy.
|
|
1072
|
+
* @param pubkey - Retained identity argument; policy is keyed by dTag and hash.
|
|
1073
|
+
* @param dTag - Verified napplet identifier.
|
|
1074
|
+
* @param aggregateHash - Verified artifact hash.
|
|
1075
|
+
* @param bytes - Integer byte count from zero through Number.MAX_SAFE_INTEGER.
|
|
1076
|
+
* @throws RangeError if bytes is not a non-negative safe integer.
|
|
1077
|
+
* @example
|
|
1078
|
+
* ```ts
|
|
1079
|
+
* acl.setStateQuota('', dTag, aggregateHash, 1024 * 1024);
|
|
1080
|
+
* acl.persist();
|
|
1081
|
+
* ```
|
|
1082
|
+
*/
|
|
1083
|
+
setStateQuota(pubkey: string, dTag: string, aggregateHash: string, bytes: number): void;
|
|
1084
|
+
/**
|
|
1085
|
+
* Save live state without rolling back live policy on failure.
|
|
1086
|
+
* @returns True if the backend completed; false if it threw.
|
|
1087
|
+
* @example
|
|
1088
|
+
* ```ts
|
|
1089
|
+
* if (!acl.persist()) showSaveError();
|
|
1090
|
+
* ```
|
|
1091
|
+
*/
|
|
1092
|
+
persist(): boolean;
|
|
1064
1093
|
load(): void;
|
|
1065
|
-
|
|
1094
|
+
/**
|
|
1095
|
+
* Reset live state to the configured default and attempt to clear storage.
|
|
1096
|
+
* @returns True if storage cleared; false if only the live reset succeeded.
|
|
1097
|
+
* @example
|
|
1098
|
+
* ```ts
|
|
1099
|
+
* if (!acl.clear()) showSaveError();
|
|
1100
|
+
* ```
|
|
1101
|
+
*/
|
|
1102
|
+
clear(): boolean;
|
|
1066
1103
|
}
|
|
1067
1104
|
/**
|
|
1068
1105
|
* Create an ACL state container backed by @kehto/acl and persisted
|
package/dist/index.js
CHANGED
|
@@ -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
|
};
|
|
@@ -1737,8 +1755,8 @@ function handleMediaMessage(context, windowId, msg) {
|
|
|
1737
1755
|
hooks.sendToNapplet(windowId, {
|
|
1738
1756
|
type: "media.session.create.result",
|
|
1739
1757
|
id: m.id ?? "",
|
|
1740
|
-
|
|
1741
|
-
|
|
1758
|
+
owner: m.owner,
|
|
1759
|
+
error: "media service unavailable"
|
|
1742
1760
|
});
|
|
1743
1761
|
}
|
|
1744
1762
|
}
|
|
@@ -2175,11 +2193,15 @@ function createRuntime(hooks) {
|
|
|
2175
2193
|
});
|
|
2176
2194
|
let incRuntime = null;
|
|
2177
2195
|
const aclState = createAclState(hooks.aclPersistence, "permissive", (mutation) => {
|
|
2178
|
-
if (mutation.type === "revoke" && mutation.capability !== "relay:read") return;
|
|
2179
2196
|
for (const entry of sessionRegistry.getAllEntries()) {
|
|
2180
2197
|
if (entry.dTag === mutation.dTag && entry.aggregateHash === mutation.aggregateHash) {
|
|
2181
|
-
|
|
2182
|
-
|
|
2198
|
+
for (const service of Object.values(serviceRegistry)) {
|
|
2199
|
+
service.onCapabilityRevoked?.(entry.windowId, mutation.type === "block" ? null : mutation.capability);
|
|
2200
|
+
}
|
|
2201
|
+
if (mutation.type === "block" || mutation.capability === "relay:read") {
|
|
2202
|
+
revokeRelaySubscriptions({ hooks, serviceRegistry, subscriptions }, entry.windowId);
|
|
2203
|
+
incRuntime?.revokeWindow(entry.windowId);
|
|
2204
|
+
}
|
|
2183
2205
|
}
|
|
2184
2206
|
}
|
|
2185
2207
|
});
|