@kehto/acl 0.1.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 ADDED
@@ -0,0 +1,97 @@
1
+ # @kehto/acl
2
+
3
+ Pure, WASM-ready ACL module for the napplet protocol — zero dependencies, zero side effects.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @kehto/acl
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ `@kehto/acl` is the authoritative access-control core for kehto. It owns an immutable `AclState` keyed on the NIP-5D 2-segment identity `(dTag, hash)` — the v1.2 canonical shape (the pre-v1.2 `(pubkey, dTag, hash)` triple is dropped; `migrateAclState` ships for legacy persistence readers).
14
+
15
+ Every function is pure: state in, state out. No I/O, no timers, no globals — the module is trivially compilable to WASM and is the single source of truth for capability decisions.
16
+
17
+ The module exposes two parallel capability surfaces:
18
+
19
+ - **Bitfield constants** (`CAP_RELAY_READ`, `CAP_RELAY_WRITE`, `CAP_STATE_READ`, `CAP_STATE_WRITE`, …) — the compact per-entry representation used inside `AclState.entries[*].caps`.
20
+ - **Canonical v1.2 NIP-5D 8-domain capability strings** (`CAP_IDENTITY_READ`, `CAP_KEYS_BIND`, `CAP_KEYS_FORWARD`, `CAP_MEDIA_CONTROL`, `CAP_NOTIFY_SEND`, `CAP_NOTIFY_CHANNEL`, `CAP_THEME_READ`) — plus the retained `relay:*`, `cache:*`, `hotkey:forward`, and `state:*` literals. These strings are what `resolveCapabilitiesNub()` returns and what `@kehto/runtime`'s enforce gate grants/revokes against. The v1.1 `sign:event`/`sign:nip04`/`sign:nip44` entries were intentionally removed — canonical NIP-5D does not expose napplet-visible signing.
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import {
26
+ createState,
27
+ grant,
28
+ check,
29
+ block,
30
+ unblock,
31
+ CAP_RELAY_WRITE,
32
+ CAP_NOTIFY_SEND,
33
+ } from '@kehto/acl';
34
+
35
+ // 1. Start with a restrictive state — unknown identities are denied everything.
36
+ let state = createState('restrictive');
37
+
38
+ const id = { dTag: 'chat', hash: 'ff00aa11' };
39
+
40
+ // 2. Grant the two canonical v1.2 capabilities this napplet needs.
41
+ state = grant(state, id, CAP_RELAY_WRITE);
42
+ state = grant(state, id, CAP_NOTIFY_SEND);
43
+
44
+ check(state, id, CAP_RELAY_WRITE); // true
45
+ check(state, id, CAP_NOTIFY_SEND); // true
46
+
47
+ // 3. Block the identity — all checks fail until unblocked, caps are preserved.
48
+ state = block(state, id);
49
+ check(state, id, CAP_RELAY_WRITE); // false (blocked)
50
+
51
+ state = unblock(state, id);
52
+ check(state, id, CAP_RELAY_WRITE); // true (restored)
53
+ ```
54
+
55
+ ## Public API
56
+
57
+ ### Types
58
+ - `AclState` — immutable ACL state container
59
+ - `AclEntry` — per-identity entry (`caps`, `blocked`, `quota`)
60
+ - `Identity` — `{ dTag, hash }` pair (NIP-5D 2-segment identity)
61
+ - `Capability` — union of every canonical capability string
62
+ - `CapabilityResolution` — `{ senderCap, recipientCap }` returned by `resolveCapabilitiesNub`
63
+ - `NubMessage` — minimal shape consumed by `resolveCapabilitiesNub` (`{ type: string }`)
64
+
65
+ ### Constants — bit flags
66
+ - `CAP_RELAY_READ`, `CAP_RELAY_WRITE`, `CAP_CACHE_READ`, `CAP_CACHE_WRITE`
67
+ - `CAP_HOTKEY_FORWARD`, `CAP_SIGN_EVENT`, `CAP_SIGN_NIP04`, `CAP_SIGN_NIP44`
68
+ - `CAP_STATE_READ`, `CAP_STATE_WRITE`, `CAP_ALL`, `CAP_NONE`
69
+ - `DEFAULT_QUOTA`
70
+
71
+ ### Constants — canonical NIP-5D capability strings (v1.2)
72
+ - `ALL_CAPABILITIES` — readonly tuple of every recognized capability string
73
+ - `CAP_IDENTITY_READ`, `CAP_KEYS_BIND`, `CAP_KEYS_FORWARD`
74
+ - `CAP_MEDIA_CONTROL`, `CAP_NOTIFY_SEND`, `CAP_NOTIFY_CHANNEL`, `CAP_THEME_READ`
75
+
76
+ ### State mutations
77
+ - [`createState`](../../docs/api/functions/_kehto_acl.createState.html) — create an empty AclState
78
+ - [`grant`](../../docs/api/functions/_kehto_acl.grant.html), [`revoke`](../../docs/api/functions/_kehto_acl.revoke.html) — add/remove capability bits
79
+ - [`block`](../../docs/api/functions/_kehto_acl.block.html), [`unblock`](../../docs/api/functions/_kehto_acl.unblock.html) — toggle the block flag
80
+ - [`setQuota`](../../docs/api/functions/_kehto_acl.setQuota.html), [`getQuota`](../../docs/api/functions/_kehto_acl.getQuota.html) — per-identity state storage quota
81
+ - [`serialize`](../../docs/api/functions/_kehto_acl.serialize.html), [`deserialize`](../../docs/api/functions/_kehto_acl.deserialize.html) — JSON round-trip for persistence
82
+
83
+ ### Capability resolution
84
+ - [`check`](../../docs/api/functions/_kehto_acl.check.html) — evaluate identity + capability against state
85
+ - [`toKey`](../../docs/api/functions/_kehto_acl.toKey.html) — compute the `dTag:hash` composite key
86
+ - [`resolveCapabilitiesNub`](../../docs/api/functions/_kehto_acl.resolveCapabilitiesNub.html) — map a NIP-5D NUB envelope type to the required sender/recipient capabilities across the 8 canonical domains
87
+
88
+ ### Migration
89
+ - [`migrateAclState`](../../docs/api/functions/_kehto_acl.migrateAclState.html) — one-shot migration from the legacy 3-segment `pubkey:dTag:hash` keys to the v1.2 2-segment `dTag:hash` keys; idempotent (returns the same reference when nothing to migrate)
90
+
91
+ ## API Reference
92
+
93
+ Full API reference: [docs/api/@kehto/acl/](../../docs/api/modules/_kehto_acl.html) (generated via `pnpm docs:api`).
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @kehto/acl — String capability constants.
3
+ *
4
+ * Canonical capability strings for NIP-5D ACL gating (v1.2 milestone).
5
+ * Complements the bit constants in types.ts (CAP_RELAY_READ etc.) — these
6
+ * string literals are what the runtime + shell read/write in grant/revoke
7
+ * paths and what resolveCapabilitiesNub returns.
8
+ *
9
+ * Zero runtime dependencies. The eight canonical NIP-5D domains are
10
+ * identity, keys, media, notify, relay, storage, ifc, theme; capabilities
11
+ * here cover each domain's gated actions.
12
+ */
13
+ /**
14
+ * All capability strings recognized by @kehto/acl.
15
+ *
16
+ * Ordering: v1.1 surface first (relay/cache/hotkey/state), then the
17
+ * v1.2 additions for the seven nubs + theme. The v1.1 `sign:event`,
18
+ * `sign:nip04`, `sign:nip44` strings were intentionally removed — no
19
+ * napplet-visible signing exists in canonical NIP-5D; signing flows
20
+ * through shell-internal `relay.publishEncrypted` instead.
21
+ */
22
+ declare const ALL_CAPABILITIES: readonly ["relay:read", "relay:write", "cache:read", "cache:write", "hotkey:forward", "state:read", "state:write", "identity:read", "keys:bind", "keys:forward", "media:control", "notify:send", "notify:channel", "theme:read"];
23
+ /** Union of every capability string in ALL_CAPABILITIES. */
24
+ type Capability = typeof ALL_CAPABILITIES[number];
25
+ /** identity.getProfile/getFollows/getList/getZaps/getMutes/getBlocked/getBadges */
26
+ declare const CAP_IDENTITY_READ: "identity:read";
27
+ /** keys.registerAction / keys.unregisterAction / keys.bindings */
28
+ declare const CAP_KEYS_BIND: "keys:bind";
29
+ /** keys.forward / keys.action */
30
+ declare const CAP_KEYS_FORWARD: "keys:forward";
31
+ /** media.* (all actions) */
32
+ declare const CAP_MEDIA_CONTROL: "media:control";
33
+ /** notify.send / notify.dismiss / notify.badge / notify.action / notify.clicked / notify.dismissed / notify.controls / notify.send.result */
34
+ declare const CAP_NOTIFY_SEND: "notify:send";
35
+ /** notify.channel.register / notify.permission.request / notify.permission.result */
36
+ declare const CAP_NOTIFY_CHANNEL: "notify:channel";
37
+ /** theme.get / theme.changed */
38
+ declare const CAP_THEME_READ: "theme:read";
39
+
40
+ export { ALL_CAPABILITIES, CAP_IDENTITY_READ, CAP_KEYS_BIND, CAP_KEYS_FORWARD, CAP_MEDIA_CONTROL, CAP_NOTIFY_CHANNEL, CAP_NOTIFY_SEND, CAP_THEME_READ, type Capability };
@@ -0,0 +1,21 @@
1
+ import {
2
+ ALL_CAPABILITIES,
3
+ CAP_IDENTITY_READ,
4
+ CAP_KEYS_BIND,
5
+ CAP_KEYS_FORWARD,
6
+ CAP_MEDIA_CONTROL,
7
+ CAP_NOTIFY_CHANNEL,
8
+ CAP_NOTIFY_SEND,
9
+ CAP_THEME_READ
10
+ } from "./chunk-7M23OTGP.js";
11
+ export {
12
+ ALL_CAPABILITIES,
13
+ CAP_IDENTITY_READ,
14
+ CAP_KEYS_BIND,
15
+ CAP_KEYS_FORWARD,
16
+ CAP_MEDIA_CONTROL,
17
+ CAP_NOTIFY_CHANNEL,
18
+ CAP_NOTIFY_SEND,
19
+ CAP_THEME_READ
20
+ };
21
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,38 @@
1
+ // src/capabilities.ts
2
+ var ALL_CAPABILITIES = [
3
+ // v1.1 kept:
4
+ "relay:read",
5
+ "relay:write",
6
+ "cache:read",
7
+ "cache:write",
8
+ "hotkey:forward",
9
+ "state:read",
10
+ "state:write",
11
+ // v1.2 additions (seven nubs + theme):
12
+ "identity:read",
13
+ "keys:bind",
14
+ "keys:forward",
15
+ "media:control",
16
+ "notify:send",
17
+ "notify:channel",
18
+ "theme:read"
19
+ ];
20
+ var CAP_IDENTITY_READ = "identity:read";
21
+ var CAP_KEYS_BIND = "keys:bind";
22
+ var CAP_KEYS_FORWARD = "keys:forward";
23
+ var CAP_MEDIA_CONTROL = "media:control";
24
+ var CAP_NOTIFY_SEND = "notify:send";
25
+ var CAP_NOTIFY_CHANNEL = "notify:channel";
26
+ var CAP_THEME_READ = "theme:read";
27
+
28
+ export {
29
+ ALL_CAPABILITIES,
30
+ CAP_IDENTITY_READ,
31
+ CAP_KEYS_BIND,
32
+ CAP_KEYS_FORWARD,
33
+ CAP_MEDIA_CONTROL,
34
+ CAP_NOTIFY_SEND,
35
+ CAP_NOTIFY_CHANNEL,
36
+ CAP_THEME_READ
37
+ };
38
+ //# sourceMappingURL=chunk-7M23OTGP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/capabilities.ts"],"sourcesContent":["/**\n * @kehto/acl — String capability constants.\n *\n * Canonical capability strings for NIP-5D ACL gating (v1.2 milestone).\n * Complements the bit constants in types.ts (CAP_RELAY_READ etc.) — these\n * string literals are what the runtime + shell read/write in grant/revoke\n * paths and what resolveCapabilitiesNub returns.\n *\n * Zero runtime dependencies. The eight canonical NIP-5D domains are\n * identity, keys, media, notify, relay, storage, ifc, theme; capabilities\n * here cover each domain's gated actions.\n */\n\n/**\n * All capability strings recognized by @kehto/acl.\n *\n * Ordering: v1.1 surface first (relay/cache/hotkey/state), then the\n * v1.2 additions for the seven nubs + theme. The v1.1 `sign:event`,\n * `sign:nip04`, `sign:nip44` strings were intentionally removed — no\n * napplet-visible signing exists in canonical NIP-5D; signing flows\n * through shell-internal `relay.publishEncrypted` instead.\n */\nexport const ALL_CAPABILITIES = [\n // v1.1 kept:\n 'relay:read', 'relay:write',\n 'cache:read', 'cache:write',\n 'hotkey:forward',\n 'state:read', 'state:write',\n // v1.2 additions (seven nubs + theme):\n 'identity:read',\n 'keys:bind', 'keys:forward',\n 'media:control',\n 'notify:send', 'notify:channel',\n 'theme:read',\n] as const;\n\n/** Union of every capability string in ALL_CAPABILITIES. */\nexport type Capability = typeof ALL_CAPABILITIES[number];\n\n// ─── Per-cap string constants (grep-friendly call sites) ────────────────────\n//\n// The v1.1 capability strings (relay:read, relay:write, cache:read,\n// cache:write, hotkey:forward, state:read, state:write) are used\n// throughout the codebase as bare literals; constants for them are\n// deliberately omitted here to keep the surface minimal.\n// The v1.2 additions below each get a named constant because they are\n// the newly-introduced surface and benefit from greppable call sites.\n\n/** identity.getProfile/getFollows/getList/getZaps/getMutes/getBlocked/getBadges */\nexport const CAP_IDENTITY_READ = 'identity:read' as const;\n/** keys.registerAction / keys.unregisterAction / keys.bindings */\nexport const CAP_KEYS_BIND = 'keys:bind' as const;\n/** keys.forward / keys.action */\nexport const CAP_KEYS_FORWARD = 'keys:forward' as const;\n/** media.* (all actions) */\nexport const CAP_MEDIA_CONTROL = 'media:control' as const;\n/** notify.send / notify.dismiss / notify.badge / notify.action / notify.clicked / notify.dismissed / notify.controls / notify.send.result */\nexport const CAP_NOTIFY_SEND = 'notify:send' as const;\n/** notify.channel.register / notify.permission.request / notify.permission.result */\nexport const CAP_NOTIFY_CHANNEL = 'notify:channel' as const;\n/** theme.get / theme.changed */\nexport const CAP_THEME_READ = 'theme:read' as const;\n"],"mappings":";AAsBO,IAAM,mBAAmB;AAAA;AAAA,EAE9B;AAAA,EAAc;AAAA,EACd;AAAA,EAAc;AAAA,EACd;AAAA,EACA;AAAA,EAAc;AAAA;AAAA,EAEd;AAAA,EACA;AAAA,EAAa;AAAA,EACb;AAAA,EACA;AAAA,EAAe;AAAA,EACf;AACF;AAeO,IAAM,oBAAsB;AAE5B,IAAM,gBAAsB;AAE5B,IAAM,mBAAsB;AAE5B,IAAM,oBAAsB;AAE5B,IAAM,kBAAsB;AAE5B,IAAM,qBAAsB;AAE5B,IAAM,iBAAsB;","names":[]}
@@ -0,0 +1,433 @@
1
+ export { ALL_CAPABILITIES, CAP_IDENTITY_READ, CAP_KEYS_BIND, CAP_KEYS_FORWARD, CAP_MEDIA_CONTROL, CAP_NOTIFY_CHANNEL, CAP_NOTIFY_SEND, CAP_THEME_READ, Capability } from './capabilities.js';
2
+
3
+ /**
4
+ * @kehto/acl — Type definitions and capability bit constants.
5
+ *
6
+ * All types use Readonly<> to enforce immutability at the type level.
7
+ * Capability constants are bitfield values for fast check/grant/revoke.
8
+ */
9
+ /** relay:read — subscribe to relay events */
10
+ declare const CAP_RELAY_READ: number;
11
+ /** relay:write — publish events to relays */
12
+ declare const CAP_RELAY_WRITE: number;
13
+ /** cache:read — read from local cache */
14
+ declare const CAP_CACHE_READ: number;
15
+ /** cache:write — write to local cache */
16
+ declare const CAP_CACHE_WRITE: number;
17
+ /** hotkey:forward — forward keyboard shortcuts to shell */
18
+ declare const CAP_HOTKEY_FORWARD: number;
19
+ /** sign:event — request event signing */
20
+ declare const CAP_SIGN_EVENT: number;
21
+ /** sign:nip04 — request NIP-04 encrypt/decrypt */
22
+ declare const CAP_SIGN_NIP04: number;
23
+ /** sign:nip44 — request NIP-44 encrypt/decrypt */
24
+ declare const CAP_SIGN_NIP44: number;
25
+ /** state:read — read napplet-scoped state */
26
+ declare const CAP_STATE_READ: number;
27
+ /** state:write — write napplet-scoped state */
28
+ declare const CAP_STATE_WRITE: number;
29
+ /** All capabilities granted (bits 0-9 set) */
30
+ declare const CAP_ALL: number;
31
+ /** No capabilities granted */
32
+ declare const CAP_NONE = 0;
33
+ /**
34
+ * Napplet identity — composite key for ACL lookups.
35
+ *
36
+ * Under NIP-5D v0.1.0, identity is assigned from the NIP-5A manifest
37
+ * at iframe creation time. The pubkey field is no longer used.
38
+ *
39
+ * @param pubkey - (deprecated) Ephemeral AUTH keypair pubkey. Ignored by toKey().
40
+ * @param dTag - Derived tag (deterministic from napp type)
41
+ * @param hash - Aggregate hash of napplet build artifacts
42
+ */
43
+ interface Identity {
44
+ /** @deprecated NIP-5D: AUTH keypair no longer exists. Pass '' or omit entirely.
45
+ * Kept as optional for backward compatibility during data migration. */
46
+ readonly pubkey?: string;
47
+ readonly dTag: string;
48
+ readonly hash: string;
49
+ }
50
+ /**
51
+ * A single ACL entry for one napplet identity.
52
+ *
53
+ * @param caps - Bitfield of granted capabilities (use CAP_* constants)
54
+ * @param blocked - Orthogonal block flag; when true, all checks fail regardless of caps
55
+ * @param quota - State storage quota in bytes
56
+ */
57
+ interface AclEntry {
58
+ readonly caps: number;
59
+ readonly blocked: boolean;
60
+ readonly quota: number;
61
+ }
62
+ /**
63
+ * Complete ACL state — immutable data structure.
64
+ *
65
+ * All mutations return a new AclState; the original is never modified.
66
+ *
67
+ * @param defaultPolicy - 'permissive' grants all caps to unknown identities;
68
+ * 'restrictive' denies all caps to unknown identities
69
+ * @param entries - Map from composite key ('dTag:hash') to AclEntry
70
+ */
71
+ interface AclState {
72
+ readonly defaultPolicy: 'permissive' | 'restrictive';
73
+ readonly entries: Readonly<Record<string, AclEntry>>;
74
+ }
75
+ /** Default state storage quota in bytes (512 KB) */
76
+ declare const DEFAULT_QUOTA: number;
77
+
78
+ /**
79
+ * @kehto/acl — Pure check function.
80
+ *
81
+ * Determines whether an identity has a specific capability.
82
+ * No side effects, no I/O, no mutations.
83
+ */
84
+
85
+ /**
86
+ * Compute composite key from identity fields.
87
+ *
88
+ * Under NIP-5D v0.1.0, the key is 'dTag:hash' (pubkey is ignored).
89
+ *
90
+ * @param identity - Napplet identity
91
+ * @returns Composite key string 'dTag:hash'
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * toKey({ dTag: 'chat', hash: 'ff00' })
96
+ * // => 'chat:ff00'
97
+ * ```
98
+ */
99
+ declare function toKey(identity: Identity): string;
100
+ /**
101
+ * Check whether an identity has a specific capability.
102
+ *
103
+ * Decision logic:
104
+ * 1. If identity has no entry: return based on defaultPolicy
105
+ * - 'permissive' → true (all caps granted to unknown identities)
106
+ * - 'restrictive' → false (all caps denied to unknown identities)
107
+ * 2. If identity is blocked: return false (blocked overrides all caps)
108
+ * 3. Otherwise: return (entry.caps & cap) !== 0
109
+ *
110
+ * @param state - Current ACL state (immutable)
111
+ * @param identity - Napplet identity to check
112
+ * @param cap - Capability bit constant (e.g., CAP_RELAY_READ)
113
+ * @returns true if the identity has the capability, false otherwise
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * import { check, createState, grant } from '@kehto/acl';
118
+ * import { CAP_RELAY_READ } from '@kehto/acl';
119
+ *
120
+ * const state = createState('restrictive');
121
+ * const id = { dTag: 'chat', hash: 'ff00' };
122
+ *
123
+ * check(state, id, CAP_RELAY_READ); // false (restrictive, no entry)
124
+ *
125
+ * const state2 = grant(state, id, CAP_RELAY_READ);
126
+ * check(state2, id, CAP_RELAY_READ); // true
127
+ * ```
128
+ */
129
+ declare function check(state: AclState, identity: Identity, cap: number): boolean;
130
+
131
+ /**
132
+ * @kehto/acl — Pure state mutation functions.
133
+ *
134
+ * Every function takes an AclState and returns a NEW AclState.
135
+ * The original state is never modified. No side effects, no I/O.
136
+ */
137
+
138
+ /**
139
+ * Create a new ACL state with the given default policy.
140
+ *
141
+ * @param policy - 'permissive' grants all caps to unknown identities;
142
+ * 'restrictive' denies all caps to unknown identities.
143
+ * Defaults to 'permissive'.
144
+ * @returns A new empty AclState
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * const state = createState('restrictive');
149
+ * // { defaultPolicy: 'restrictive', entries: {} }
150
+ * ```
151
+ */
152
+ declare function createState(policy?: 'permissive' | 'restrictive'): AclState;
153
+ /**
154
+ * Grant a capability to an identity.
155
+ *
156
+ * If the identity has no entry, one is created with default caps plus the granted cap.
157
+ * Returns a new AclState — the original is not modified.
158
+ *
159
+ * @param state - Current ACL state
160
+ * @param identity - Napplet identity
161
+ * @param cap - Capability bit constant to grant (e.g., CAP_RELAY_READ)
162
+ * @returns New AclState with the capability granted
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const state2 = grant(state, id, CAP_RELAY_READ);
167
+ * check(state2, id, CAP_RELAY_READ); // true
168
+ * ```
169
+ */
170
+ declare function grant(state: AclState, identity: Identity, cap: number): AclState;
171
+ /**
172
+ * Revoke a capability from an identity.
173
+ *
174
+ * If the identity has no entry, one is created with default caps minus the revoked cap.
175
+ * Returns a new AclState — the original is not modified.
176
+ *
177
+ * @param state - Current ACL state
178
+ * @param identity - Napplet identity
179
+ * @param cap - Capability bit constant to revoke (e.g., CAP_RELAY_WRITE)
180
+ * @returns New AclState with the capability revoked
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const state2 = revoke(state, id, CAP_RELAY_WRITE);
185
+ * check(state2, id, CAP_RELAY_WRITE); // false
186
+ * ```
187
+ */
188
+ declare function revoke(state: AclState, identity: Identity, cap: number): AclState;
189
+ /**
190
+ * Block an identity.
191
+ *
192
+ * A blocked identity fails all capability checks regardless of granted caps.
193
+ * The caps bitfield is preserved — unblocking restores previous capabilities.
194
+ * Returns a new AclState — the original is not modified.
195
+ *
196
+ * @param state - Current ACL state
197
+ * @param identity - Napplet identity to block
198
+ * @returns New AclState with the identity blocked
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const state2 = block(state, id);
203
+ * check(state2, id, CAP_RELAY_READ); // false (blocked)
204
+ * ```
205
+ */
206
+ declare function block(state: AclState, identity: Identity): AclState;
207
+ /**
208
+ * Unblock an identity.
209
+ *
210
+ * Restores capability checks to use the caps bitfield.
211
+ * Returns a new AclState — the original is not modified.
212
+ *
213
+ * @param state - Current ACL state
214
+ * @param identity - Napplet identity to unblock
215
+ * @returns New AclState with the identity unblocked
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * const state2 = unblock(state, id);
220
+ * check(state2, id, CAP_RELAY_READ); // true (if cap was granted)
221
+ * ```
222
+ */
223
+ declare function unblock(state: AclState, identity: Identity): AclState;
224
+ /**
225
+ * Set the state storage quota for an identity.
226
+ *
227
+ * @param state - Current ACL state
228
+ * @param identity - Napplet identity
229
+ * @param bytes - Quota in bytes
230
+ * @returns New AclState with the quota set
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * const state2 = setQuota(state, id, 1024 * 1024); // 1 MB
235
+ * getQuota(state2, id); // 1048576
236
+ * ```
237
+ */
238
+ declare function setQuota(state: AclState, identity: Identity, bytes: number): AclState;
239
+ /**
240
+ * Get the state storage quota for an identity.
241
+ *
242
+ * Returns DEFAULT_QUOTA (512 KB) if no entry exists.
243
+ *
244
+ * @param state - Current ACL state
245
+ * @param identity - Napplet identity
246
+ * @returns Quota in bytes
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * getQuota(state, id); // 524288 (default 512 KB)
251
+ * ```
252
+ */
253
+ declare function getQuota(state: AclState, identity: Identity): number;
254
+ /**
255
+ * Serialize ACL state to a JSON string.
256
+ *
257
+ * Pure function — no I/O. The persistence adapter in @kehto/shell
258
+ * uses this to write state to localStorage or other backends.
259
+ *
260
+ * @param state - ACL state to serialize
261
+ * @returns JSON string representation
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * const json = serialize(state);
266
+ * localStorage.setItem('napplet:acl', json);
267
+ * ```
268
+ */
269
+ declare function serialize(state: AclState): string;
270
+ /**
271
+ * Deserialize ACL state from a JSON string.
272
+ *
273
+ * Pure function — no I/O. Returns a valid AclState or a fresh
274
+ * permissive state if the input is invalid.
275
+ *
276
+ * @param json - JSON string to parse
277
+ * @returns Parsed AclState, or fresh permissive state on parse failure
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * const json = localStorage.getItem('napplet:acl') ?? '';
282
+ * const state = deserialize(json);
283
+ * ```
284
+ */
285
+ declare function deserialize(json: string): AclState;
286
+
287
+ /**
288
+ * @kehto/acl — ACL state migration utility.
289
+ *
290
+ * Provides a pure function to migrate persisted ACL state from the old
291
+ * 3-segment composite key format (pubkey:dTag:hash) to the new 2-segment
292
+ * format (dTag:hash) introduced in NIP-5D v0.1.0.
293
+ *
294
+ * No I/O, no side effects. Pure function: takes AclState, returns AclState.
295
+ */
296
+
297
+ /**
298
+ * Migrate ACL state from old 3-segment key format to new 2-segment key format.
299
+ *
300
+ * Converts entries stored under 'pubkey:dTag:hash' keys to 'dTag:hash' keys.
301
+ * If two old entries map to the same dTag:hash, merges them conservatively:
302
+ * - caps: OR of both bitfields (never removes a granted capability)
303
+ * - blocked: OR of both flags (blocks if either source was blocked)
304
+ * - quota: MAX of both values (keeps the higher allocation)
305
+ *
306
+ * Idempotent: if no 3-segment keys are found, returns the original state
307
+ * unchanged (same object reference).
308
+ *
309
+ * @param state - Current ACL state (may contain old-format entries)
310
+ * @returns Migrated AclState with only 2-segment keys, or the original
311
+ * state unchanged if no migration was needed
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * const oldState = deserialize(localStorage.getItem('napplet:acl') ?? '');
316
+ * const newState = migrateAclState(oldState);
317
+ * if (newState !== oldState) {
318
+ * // Migration occurred — persist the new format
319
+ * localStorage.setItem('napplet:acl', serialize(newState));
320
+ * }
321
+ * ```
322
+ */
323
+ declare function migrateAclState(state: AclState): AclState;
324
+
325
+ /**
326
+ * @kehto/acl — NUB domain capability resolution (8-domain canonical).
327
+ *
328
+ * Maps NUB message types (e.g., 'relay.subscribe', 'identity.getProfile') to
329
+ * the capability strings required by sender and recipient. This is the
330
+ * canonical source for "which capability does this NUB operation require?"
331
+ * in the @kehto/acl package.
332
+ *
333
+ * Canonical NIP-5D 8 domains: identity, keys, media, notify, relay,
334
+ * storage, ifc, theme. The v1.1 `signer` domain is REMOVED — getPublicKey/
335
+ * getRelays migrated to `identity`; signEvent/nip04/nip44 have no
336
+ * napplet-visible surface (shell handles encryption inside
337
+ * `relay.publishEncrypted`).
338
+ *
339
+ * Zero dependencies. No imports from @napplet/core or any external package.
340
+ *
341
+ * @see packages/acl/src/capabilities.ts for cap string constants + ALL_CAPABILITIES.
342
+ * @see docs/ACL-MIGRATION.md section 2 — Capability Constant to NUB Domain Mapping.
343
+ */
344
+ /**
345
+ * Minimal message shape used for capability resolution.
346
+ *
347
+ * Compatible with NappletMessage from @napplet/core, but defined here
348
+ * independently to maintain @kehto/acl's zero-dependency constraint.
349
+ *
350
+ * @param type - NUB message type, e.g. 'relay.subscribe', 'identity.getProfile'
351
+ */
352
+ interface NubMessage {
353
+ readonly type: string;
354
+ }
355
+ /**
356
+ * Result of resolving what capabilities a NUB message requires.
357
+ *
358
+ * | Field | Description |
359
+ * |----------------|----------------------------------------------------------------|
360
+ * | `senderCap` | Capability the sender must have, or null if no check needed |
361
+ * | `recipientCap` | Capability the recipient must have, or null if no check needed |
362
+ *
363
+ * @param senderCap - Capability the sender must have, or null if no ACL gate required
364
+ * @param recipientCap - Capability the recipient must have, or null if no recipient check
365
+ */
366
+ interface CapabilityResolution {
367
+ readonly senderCap: string | null;
368
+ readonly recipientCap: string | null;
369
+ }
370
+ /**
371
+ * Resolve the capabilities required by a NUB message.
372
+ *
373
+ * Splits `msg.type` on '.' to obtain `[domain, action]`, then dispatches to
374
+ * a per-domain mapper. Unknown domains return `null/null` (silently ignored).
375
+ *
376
+ * **NUB domain mapping table (8 canonical domains):**
377
+ *
378
+ * | Domain | Action(s) | senderCap | recipientCap |
379
+ * |------------|--------------------------------------------------------------|-----------------|---------------|
380
+ * | `relay` | `subscribe`, `query`, `close`, results/pushes | `relay:read` | `null` |
381
+ * | `relay` | `publish` | `relay:write` | `relay:read` |
382
+ * | `relay` | `publishEncrypted` | `relay:write` | `null` |
383
+ * | `identity` | `getPublicKey`, `getRelays` | `null` | `null` |
384
+ * | `identity` | `getProfile/getFollows/getList/getZaps/getMutes/...` | `identity:read` | `null` |
385
+ * | `keys` | `forward`, `action` | `keys:forward` | `null` |
386
+ * | `keys` | `registerAction`, `unregisterAction`, `bindings` | `keys:bind` | `null` |
387
+ * | `media` | any | `media:control` | `null` |
388
+ * | `notify` | `channel.register`, `permission.request`, `permission.result` | `notify:channel`| `null` |
389
+ * | `notify` | `send`, `dismiss`, `badge`, `clicked`, `action`, ... | `notify:send` | `null` |
390
+ * | `storage` | `get`, `keys` | `state:read` | `null` |
391
+ * | `storage` | `set`, `remove` | `state:write` | `null` |
392
+ * | `storage` | any other (incl. removed `clear`) | `null` | `null` |
393
+ * | `ifc` | `emit`, `channel.emit`, `channel.broadcast` | `relay:write` | `relay:read` |
394
+ * | `ifc` | `subscribe`, `unsubscribe`, `channel.open/list/close` | `relay:read` | `null` |
395
+ * | `theme` | `get`, `get.result` | `theme:read` | `null` |
396
+ * | `theme` | `changed` (shell → napplet push) | `null` | `theme:read` |
397
+ * | unknown | any | `null` | `null` |
398
+ *
399
+ * The `signer` domain is REMOVED — signer messages fall through to the
400
+ * default null/null branch. `getPublicKey`/`getRelays` migrated to
401
+ * `identity`; napplet-visible signing does not exist in NIP-5D (shell
402
+ * signs internally for `relay.publishEncrypted`).
403
+ *
404
+ * @param msg - Message with a `type` field in NUB format (e.g., 'relay.subscribe')
405
+ * @returns CapabilityResolution with senderCap and recipientCap (each may be null)
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * resolveCapabilitiesNub({ type: 'relay.subscribe' })
410
+ * // => { senderCap: 'relay:read', recipientCap: null }
411
+ *
412
+ * resolveCapabilitiesNub({ type: 'relay.publishEncrypted' })
413
+ * // => { senderCap: 'relay:write', recipientCap: null }
414
+ *
415
+ * resolveCapabilitiesNub({ type: 'identity.getProfile' })
416
+ * // => { senderCap: 'identity:read', recipientCap: null }
417
+ *
418
+ * resolveCapabilitiesNub({ type: 'keys.forward' })
419
+ * // => { senderCap: 'keys:forward', recipientCap: null }
420
+ *
421
+ * resolveCapabilitiesNub({ type: 'ifc.channel.broadcast' })
422
+ * // => { senderCap: 'relay:write', recipientCap: 'relay:read' }
423
+ *
424
+ * resolveCapabilitiesNub({ type: 'theme.changed' })
425
+ * // => { senderCap: null, recipientCap: 'theme:read' }
426
+ *
427
+ * resolveCapabilitiesNub({ type: 'signer.signEvent' })
428
+ * // => { senderCap: null, recipientCap: null } // domain removed
429
+ * ```
430
+ */
431
+ declare function resolveCapabilitiesNub(msg: NubMessage): CapabilityResolution;
432
+
433
+ export { type AclEntry, type AclState, CAP_ALL, CAP_CACHE_READ, CAP_CACHE_WRITE, CAP_HOTKEY_FORWARD, CAP_NONE, CAP_RELAY_READ, CAP_RELAY_WRITE, CAP_SIGN_EVENT, CAP_SIGN_NIP04, CAP_SIGN_NIP44, CAP_STATE_READ, CAP_STATE_WRITE, type CapabilityResolution, DEFAULT_QUOTA, type Identity, type NubMessage, block, check, createState, deserialize, getQuota, grant, migrateAclState, resolveCapabilitiesNub, revoke, serialize, setQuota, toKey, unblock };
package/dist/index.js ADDED
@@ -0,0 +1,278 @@
1
+ import {
2
+ ALL_CAPABILITIES,
3
+ CAP_IDENTITY_READ,
4
+ CAP_KEYS_BIND,
5
+ CAP_KEYS_FORWARD,
6
+ CAP_MEDIA_CONTROL,
7
+ CAP_NOTIFY_CHANNEL,
8
+ CAP_NOTIFY_SEND,
9
+ CAP_THEME_READ
10
+ } from "./chunk-7M23OTGP.js";
11
+
12
+ // src/types.ts
13
+ var CAP_RELAY_READ = 1 << 0;
14
+ var CAP_RELAY_WRITE = 1 << 1;
15
+ var CAP_CACHE_READ = 1 << 2;
16
+ var CAP_CACHE_WRITE = 1 << 3;
17
+ var CAP_HOTKEY_FORWARD = 1 << 4;
18
+ var CAP_SIGN_EVENT = 1 << 5;
19
+ var CAP_SIGN_NIP04 = 1 << 6;
20
+ var CAP_SIGN_NIP44 = 1 << 7;
21
+ var CAP_STATE_READ = 1 << 8;
22
+ var CAP_STATE_WRITE = 1 << 9;
23
+ var CAP_ALL = (1 << 10) - 1;
24
+ var CAP_NONE = 0;
25
+ var DEFAULT_QUOTA = 512 * 1024;
26
+
27
+ // src/check.ts
28
+ function toKey(identity) {
29
+ return `${identity.dTag}:${identity.hash}`;
30
+ }
31
+ function check(state, identity, cap) {
32
+ const key = toKey(identity);
33
+ const entry = state.entries[key];
34
+ if (!entry) {
35
+ return state.defaultPolicy === "permissive";
36
+ }
37
+ if (entry.blocked) {
38
+ return false;
39
+ }
40
+ return (entry.caps & cap) !== 0;
41
+ }
42
+
43
+ // src/mutations.ts
44
+ function createState(policy = "permissive") {
45
+ return { defaultPolicy: policy, entries: {} };
46
+ }
47
+ function getEntry(state, key) {
48
+ const existing = state.entries[key];
49
+ if (existing) return existing;
50
+ return {
51
+ caps: state.defaultPolicy === "permissive" ? CAP_ALL : 0,
52
+ blocked: false,
53
+ quota: DEFAULT_QUOTA
54
+ };
55
+ }
56
+ function grant(state, identity, cap) {
57
+ const key = toKey(identity);
58
+ const entry = getEntry(state, key);
59
+ return {
60
+ ...state,
61
+ entries: {
62
+ ...state.entries,
63
+ [key]: { ...entry, caps: entry.caps | cap }
64
+ }
65
+ };
66
+ }
67
+ function revoke(state, identity, cap) {
68
+ const key = toKey(identity);
69
+ const entry = getEntry(state, key);
70
+ return {
71
+ ...state,
72
+ entries: {
73
+ ...state.entries,
74
+ [key]: { ...entry, caps: entry.caps & ~cap }
75
+ }
76
+ };
77
+ }
78
+ function block(state, identity) {
79
+ const key = toKey(identity);
80
+ const entry = getEntry(state, key);
81
+ return {
82
+ ...state,
83
+ entries: {
84
+ ...state.entries,
85
+ [key]: { ...entry, blocked: true }
86
+ }
87
+ };
88
+ }
89
+ function unblock(state, identity) {
90
+ const key = toKey(identity);
91
+ const entry = getEntry(state, key);
92
+ return {
93
+ ...state,
94
+ entries: {
95
+ ...state.entries,
96
+ [key]: { ...entry, blocked: false }
97
+ }
98
+ };
99
+ }
100
+ function setQuota(state, identity, bytes) {
101
+ const key = toKey(identity);
102
+ const entry = getEntry(state, key);
103
+ return {
104
+ ...state,
105
+ entries: {
106
+ ...state.entries,
107
+ [key]: { ...entry, quota: bytes }
108
+ }
109
+ };
110
+ }
111
+ function getQuota(state, identity) {
112
+ const key = toKey(identity);
113
+ const entry = state.entries[key];
114
+ return entry?.quota ?? DEFAULT_QUOTA;
115
+ }
116
+ function serialize(state) {
117
+ return JSON.stringify(state);
118
+ }
119
+ function deserialize(json) {
120
+ try {
121
+ const parsed = JSON.parse(json);
122
+ if (typeof parsed === "object" && parsed !== null && (parsed.defaultPolicy === "permissive" || parsed.defaultPolicy === "restrictive") && typeof parsed.entries === "object" && parsed.entries !== null) {
123
+ const entries = {};
124
+ for (const [key, value] of Object.entries(parsed.entries)) {
125
+ const entry = value;
126
+ if (typeof entry.caps === "number" && typeof entry.blocked === "boolean" && typeof entry.quota === "number") {
127
+ entries[key] = {
128
+ caps: entry.caps,
129
+ blocked: entry.blocked,
130
+ quota: entry.quota
131
+ };
132
+ }
133
+ }
134
+ return { defaultPolicy: parsed.defaultPolicy, entries };
135
+ }
136
+ } catch {
137
+ }
138
+ return createState("permissive");
139
+ }
140
+
141
+ // src/migrate.ts
142
+ function migrateAclState(state) {
143
+ const newEntries = {};
144
+ let migrated = false;
145
+ for (const [key, entry] of Object.entries(state.entries)) {
146
+ const parts = key.split(":");
147
+ if (parts.length === 3) {
148
+ const newKey = `${parts[1]}:${parts[2]}`;
149
+ const existing = newEntries[newKey];
150
+ if (existing) {
151
+ newEntries[newKey] = {
152
+ caps: existing.caps | entry.caps,
153
+ blocked: existing.blocked || entry.blocked,
154
+ quota: Math.max(existing.quota, entry.quota)
155
+ };
156
+ } else {
157
+ newEntries[newKey] = entry;
158
+ }
159
+ migrated = true;
160
+ } else {
161
+ const existing = newEntries[key];
162
+ if (existing) {
163
+ newEntries[key] = {
164
+ caps: existing.caps | entry.caps,
165
+ blocked: existing.blocked || entry.blocked,
166
+ quota: Math.max(existing.quota, entry.quota)
167
+ };
168
+ } else {
169
+ newEntries[key] = entry;
170
+ }
171
+ }
172
+ }
173
+ if (!migrated) return state;
174
+ return { defaultPolicy: state.defaultPolicy, entries: newEntries };
175
+ }
176
+
177
+ // src/resolve.ts
178
+ function relayMap(action) {
179
+ if (action === "publish") return { senderCap: "relay:write", recipientCap: "relay:read" };
180
+ if (action === "publishEncrypted") return { senderCap: "relay:write", recipientCap: null };
181
+ return { senderCap: "relay:read", recipientCap: null };
182
+ }
183
+ function identityMap(action) {
184
+ if (action === "getPublicKey" || action === "getRelays") {
185
+ return { senderCap: null, recipientCap: null };
186
+ }
187
+ return { senderCap: "identity:read", recipientCap: null };
188
+ }
189
+ function keysMap(action) {
190
+ if (action === "forward" || action === "action") {
191
+ return { senderCap: "keys:forward", recipientCap: null };
192
+ }
193
+ return { senderCap: "keys:bind", recipientCap: null };
194
+ }
195
+ function notifyMap(action) {
196
+ if (action === "channel.register" || action === "permission.request" || action === "permission.result") {
197
+ return { senderCap: "notify:channel", recipientCap: null };
198
+ }
199
+ return { senderCap: "notify:send", recipientCap: null };
200
+ }
201
+ function storageMap(action) {
202
+ if (action === "get" || action === "keys") return { senderCap: "state:read", recipientCap: null };
203
+ if (action === "set" || action === "remove") return { senderCap: "state:write", recipientCap: null };
204
+ return { senderCap: null, recipientCap: null };
205
+ }
206
+ function ifcMap(action) {
207
+ if (action === "emit" || action === "channel.emit" || action === "channel.broadcast") {
208
+ return { senderCap: "relay:write", recipientCap: "relay:read" };
209
+ }
210
+ return { senderCap: "relay:read", recipientCap: null };
211
+ }
212
+ function themeMap(action) {
213
+ if (action === "changed") return { senderCap: null, recipientCap: "theme:read" };
214
+ return { senderCap: "theme:read", recipientCap: null };
215
+ }
216
+ function resolveCapabilitiesNub(msg) {
217
+ const dotIdx = msg.type.indexOf(".");
218
+ if (dotIdx === -1) return { senderCap: null, recipientCap: null };
219
+ const domain = msg.type.slice(0, dotIdx);
220
+ const action = msg.type.slice(dotIdx + 1);
221
+ switch (domain) {
222
+ case "relay":
223
+ return relayMap(action);
224
+ case "identity":
225
+ return identityMap(action);
226
+ case "keys":
227
+ return keysMap(action);
228
+ case "media":
229
+ return { senderCap: "media:control", recipientCap: null };
230
+ case "notify":
231
+ return notifyMap(action);
232
+ case "storage":
233
+ return storageMap(action);
234
+ case "ifc":
235
+ return ifcMap(action);
236
+ case "theme":
237
+ return themeMap(action);
238
+ default:
239
+ return { senderCap: null, recipientCap: null };
240
+ }
241
+ }
242
+ export {
243
+ ALL_CAPABILITIES,
244
+ CAP_ALL,
245
+ CAP_CACHE_READ,
246
+ CAP_CACHE_WRITE,
247
+ CAP_HOTKEY_FORWARD,
248
+ CAP_IDENTITY_READ,
249
+ CAP_KEYS_BIND,
250
+ CAP_KEYS_FORWARD,
251
+ CAP_MEDIA_CONTROL,
252
+ CAP_NONE,
253
+ CAP_NOTIFY_CHANNEL,
254
+ CAP_NOTIFY_SEND,
255
+ CAP_RELAY_READ,
256
+ CAP_RELAY_WRITE,
257
+ CAP_SIGN_EVENT,
258
+ CAP_SIGN_NIP04,
259
+ CAP_SIGN_NIP44,
260
+ CAP_STATE_READ,
261
+ CAP_STATE_WRITE,
262
+ CAP_THEME_READ,
263
+ DEFAULT_QUOTA,
264
+ block,
265
+ check,
266
+ createState,
267
+ deserialize,
268
+ getQuota,
269
+ grant,
270
+ migrateAclState,
271
+ resolveCapabilitiesNub,
272
+ revoke,
273
+ serialize,
274
+ setQuota,
275
+ toKey,
276
+ unblock
277
+ };
278
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/check.ts","../src/mutations.ts","../src/migrate.ts","../src/resolve.ts"],"sourcesContent":["/**\n * @kehto/acl — Type definitions and capability bit constants.\n *\n * All types use Readonly<> to enforce immutability at the type level.\n * Capability constants are bitfield values for fast check/grant/revoke.\n */\n\n// ─── Capability Bit Constants ─────────────────────────────────────────────────\n\n/** relay:read — subscribe to relay events */\nexport const CAP_RELAY_READ = 1 << 0; // 1\n/** relay:write — publish events to relays */\nexport const CAP_RELAY_WRITE = 1 << 1; // 2\n/** cache:read — read from local cache */\nexport const CAP_CACHE_READ = 1 << 2; // 4\n/** cache:write — write to local cache */\nexport const CAP_CACHE_WRITE = 1 << 3; // 8\n/** hotkey:forward — forward keyboard shortcuts to shell */\nexport const CAP_HOTKEY_FORWARD = 1 << 4; // 16\n/** sign:event — request event signing */\nexport const CAP_SIGN_EVENT = 1 << 5; // 32\n/** sign:nip04 — request NIP-04 encrypt/decrypt */\nexport const CAP_SIGN_NIP04 = 1 << 6; // 64\n/** sign:nip44 — request NIP-44 encrypt/decrypt */\nexport const CAP_SIGN_NIP44 = 1 << 7; // 128\n/** state:read — read napplet-scoped state */\nexport const CAP_STATE_READ = 1 << 8; // 256\n/** state:write — write napplet-scoped state */\nexport const CAP_STATE_WRITE = 1 << 9; // 512\n\n/** All capabilities granted (bits 0-9 set) */\nexport const CAP_ALL = (1 << 10) - 1; // 1023\n\n/** No capabilities granted */\nexport const CAP_NONE = 0;\n\n// ─── Identity ─────────────────────────────────────────────────────────────────\n\n/**\n * Napplet identity — composite key for ACL lookups.\n *\n * Under NIP-5D v0.1.0, identity is assigned from the NIP-5A manifest\n * at iframe creation time. The pubkey field is no longer used.\n *\n * @param pubkey - (deprecated) Ephemeral AUTH keypair pubkey. Ignored by toKey().\n * @param dTag - Derived tag (deterministic from napp type)\n * @param hash - Aggregate hash of napplet build artifacts\n */\nexport interface Identity {\n /** @deprecated NIP-5D: AUTH keypair no longer exists. Pass '' or omit entirely.\n * Kept as optional for backward compatibility during data migration. */\n readonly pubkey?: string;\n readonly dTag: string;\n readonly hash: string;\n}\n\n// ─── ACL Entry ────────────────────────────────────────────────────────────────\n\n/**\n * A single ACL entry for one napplet identity.\n *\n * @param caps - Bitfield of granted capabilities (use CAP_* constants)\n * @param blocked - Orthogonal block flag; when true, all checks fail regardless of caps\n * @param quota - State storage quota in bytes\n */\nexport interface AclEntry {\n readonly caps: number;\n readonly blocked: boolean;\n readonly quota: number;\n}\n\n// ─── ACL State ────────────────────────────────────────────────────────────────\n\n/**\n * Complete ACL state — immutable data structure.\n *\n * All mutations return a new AclState; the original is never modified.\n *\n * @param defaultPolicy - 'permissive' grants all caps to unknown identities;\n * 'restrictive' denies all caps to unknown identities\n * @param entries - Map from composite key ('dTag:hash') to AclEntry\n */\nexport interface AclState {\n readonly defaultPolicy: 'permissive' | 'restrictive';\n readonly entries: Readonly<Record<string, AclEntry>>;\n}\n\n/** Default state storage quota in bytes (512 KB) */\nexport const DEFAULT_QUOTA = 512 * 1024;\n","/**\n * @kehto/acl — Pure check function.\n *\n * Determines whether an identity has a specific capability.\n * No side effects, no I/O, no mutations.\n */\n\nimport type { AclState, Identity } from './types.js';\n\n/**\n * Compute composite key from identity fields.\n *\n * Under NIP-5D v0.1.0, the key is 'dTag:hash' (pubkey is ignored).\n *\n * @param identity - Napplet identity\n * @returns Composite key string 'dTag:hash'\n *\n * @example\n * ```ts\n * toKey({ dTag: 'chat', hash: 'ff00' })\n * // => 'chat:ff00'\n * ```\n */\nexport function toKey(identity: Identity): string {\n return `${identity.dTag}:${identity.hash}`;\n}\n\n/**\n * Check whether an identity has a specific capability.\n *\n * Decision logic:\n * 1. If identity has no entry: return based on defaultPolicy\n * - 'permissive' → true (all caps granted to unknown identities)\n * - 'restrictive' → false (all caps denied to unknown identities)\n * 2. If identity is blocked: return false (blocked overrides all caps)\n * 3. Otherwise: return (entry.caps & cap) !== 0\n *\n * @param state - Current ACL state (immutable)\n * @param identity - Napplet identity to check\n * @param cap - Capability bit constant (e.g., CAP_RELAY_READ)\n * @returns true if the identity has the capability, false otherwise\n *\n * @example\n * ```ts\n * import { check, createState, grant } from '@kehto/acl';\n * import { CAP_RELAY_READ } from '@kehto/acl';\n *\n * const state = createState('restrictive');\n * const id = { dTag: 'chat', hash: 'ff00' };\n *\n * check(state, id, CAP_RELAY_READ); // false (restrictive, no entry)\n *\n * const state2 = grant(state, id, CAP_RELAY_READ);\n * check(state2, id, CAP_RELAY_READ); // true\n * ```\n */\nexport function check(state: AclState, identity: Identity, cap: number): boolean {\n const key = toKey(identity);\n const entry = state.entries[key];\n if (!entry) {\n return state.defaultPolicy === 'permissive';\n }\n if (entry.blocked) {\n return false;\n }\n return (entry.caps & cap) !== 0;\n}\n","/**\n * @kehto/acl — Pure state mutation functions.\n *\n * Every function takes an AclState and returns a NEW AclState.\n * The original state is never modified. No side effects, no I/O.\n */\n\nimport type { AclState, AclEntry, Identity } from './types.js';\nimport { CAP_ALL, DEFAULT_QUOTA } from './types.js';\nimport { toKey } from './check.js';\n\n/**\n * Create a new ACL state with the given default policy.\n *\n * @param policy - 'permissive' grants all caps to unknown identities;\n * 'restrictive' denies all caps to unknown identities.\n * Defaults to 'permissive'.\n * @returns A new empty AclState\n *\n * @example\n * ```ts\n * const state = createState('restrictive');\n * // { defaultPolicy: 'restrictive', entries: {} }\n * ```\n */\nexport function createState(policy: 'permissive' | 'restrictive' = 'permissive'): AclState {\n return { defaultPolicy: policy, entries: {} };\n}\n\n/**\n * Get the entry for an identity, or a default entry based on policy.\n * Internal helper — not exported.\n */\nfunction getEntry(state: AclState, key: string): AclEntry {\n const existing = state.entries[key];\n if (existing) return existing;\n // Default entry: all caps if permissive, no caps if restrictive\n return {\n caps: state.defaultPolicy === 'permissive' ? CAP_ALL : 0,\n blocked: false,\n quota: DEFAULT_QUOTA,\n };\n}\n\n/**\n * Grant a capability to an identity.\n *\n * If the identity has no entry, one is created with default caps plus the granted cap.\n * Returns a new AclState — the original is not modified.\n *\n * @param state - Current ACL state\n * @param identity - Napplet identity\n * @param cap - Capability bit constant to grant (e.g., CAP_RELAY_READ)\n * @returns New AclState with the capability granted\n *\n * @example\n * ```ts\n * const state2 = grant(state, id, CAP_RELAY_READ);\n * check(state2, id, CAP_RELAY_READ); // true\n * ```\n */\nexport function grant(state: AclState, identity: Identity, cap: number): AclState {\n const key = toKey(identity);\n const entry = getEntry(state, key);\n return {\n ...state,\n entries: {\n ...state.entries,\n [key]: { ...entry, caps: entry.caps | cap },\n },\n };\n}\n\n/**\n * Revoke a capability from an identity.\n *\n * If the identity has no entry, one is created with default caps minus the revoked cap.\n * Returns a new AclState — the original is not modified.\n *\n * @param state - Current ACL state\n * @param identity - Napplet identity\n * @param cap - Capability bit constant to revoke (e.g., CAP_RELAY_WRITE)\n * @returns New AclState with the capability revoked\n *\n * @example\n * ```ts\n * const state2 = revoke(state, id, CAP_RELAY_WRITE);\n * check(state2, id, CAP_RELAY_WRITE); // false\n * ```\n */\nexport function revoke(state: AclState, identity: Identity, cap: number): AclState {\n const key = toKey(identity);\n const entry = getEntry(state, key);\n return {\n ...state,\n entries: {\n ...state.entries,\n [key]: { ...entry, caps: entry.caps & ~cap },\n },\n };\n}\n\n/**\n * Block an identity.\n *\n * A blocked identity fails all capability checks regardless of granted caps.\n * The caps bitfield is preserved — unblocking restores previous capabilities.\n * Returns a new AclState — the original is not modified.\n *\n * @param state - Current ACL state\n * @param identity - Napplet identity to block\n * @returns New AclState with the identity blocked\n *\n * @example\n * ```ts\n * const state2 = block(state, id);\n * check(state2, id, CAP_RELAY_READ); // false (blocked)\n * ```\n */\nexport function block(state: AclState, identity: Identity): AclState {\n const key = toKey(identity);\n const entry = getEntry(state, key);\n return {\n ...state,\n entries: {\n ...state.entries,\n [key]: { ...entry, blocked: true },\n },\n };\n}\n\n/**\n * Unblock an identity.\n *\n * Restores capability checks to use the caps bitfield.\n * Returns a new AclState — the original is not modified.\n *\n * @param state - Current ACL state\n * @param identity - Napplet identity to unblock\n * @returns New AclState with the identity unblocked\n *\n * @example\n * ```ts\n * const state2 = unblock(state, id);\n * check(state2, id, CAP_RELAY_READ); // true (if cap was granted)\n * ```\n */\nexport function unblock(state: AclState, identity: Identity): AclState {\n const key = toKey(identity);\n const entry = getEntry(state, key);\n return {\n ...state,\n entries: {\n ...state.entries,\n [key]: { ...entry, blocked: false },\n },\n };\n}\n\n/**\n * Set the state storage quota for an identity.\n *\n * @param state - Current ACL state\n * @param identity - Napplet identity\n * @param bytes - Quota in bytes\n * @returns New AclState with the quota set\n *\n * @example\n * ```ts\n * const state2 = setQuota(state, id, 1024 * 1024); // 1 MB\n * getQuota(state2, id); // 1048576\n * ```\n */\nexport function setQuota(state: AclState, identity: Identity, bytes: number): AclState {\n const key = toKey(identity);\n const entry = getEntry(state, key);\n return {\n ...state,\n entries: {\n ...state.entries,\n [key]: { ...entry, quota: bytes },\n },\n };\n}\n\n/**\n * Get the state storage quota for an identity.\n *\n * Returns DEFAULT_QUOTA (512 KB) if no entry exists.\n *\n * @param state - Current ACL state\n * @param identity - Napplet identity\n * @returns Quota in bytes\n *\n * @example\n * ```ts\n * getQuota(state, id); // 524288 (default 512 KB)\n * ```\n */\nexport function getQuota(state: AclState, identity: Identity): number {\n const key = toKey(identity);\n const entry = state.entries[key];\n return entry?.quota ?? DEFAULT_QUOTA;\n}\n\n/**\n * Serialize ACL state to a JSON string.\n *\n * Pure function — no I/O. The persistence adapter in @kehto/shell\n * uses this to write state to localStorage or other backends.\n *\n * @param state - ACL state to serialize\n * @returns JSON string representation\n *\n * @example\n * ```ts\n * const json = serialize(state);\n * localStorage.setItem('napplet:acl', json);\n * ```\n */\nexport function serialize(state: AclState): string {\n return JSON.stringify(state);\n}\n\n/**\n * Deserialize ACL state from a JSON string.\n *\n * Pure function — no I/O. Returns a valid AclState or a fresh\n * permissive state if the input is invalid.\n *\n * @param json - JSON string to parse\n * @returns Parsed AclState, or fresh permissive state on parse failure\n *\n * @example\n * ```ts\n * const json = localStorage.getItem('napplet:acl') ?? '';\n * const state = deserialize(json);\n * ```\n */\nexport function deserialize(json: string): AclState {\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n (parsed.defaultPolicy === 'permissive' || parsed.defaultPolicy === 'restrictive') &&\n typeof parsed.entries === 'object' &&\n parsed.entries !== null\n ) {\n // Validate each entry\n const entries: Record<string, AclEntry> = {};\n for (const [key, value] of Object.entries(parsed.entries)) {\n const entry = value as Record<string, unknown>;\n if (\n typeof entry.caps === 'number' &&\n typeof entry.blocked === 'boolean' &&\n typeof entry.quota === 'number'\n ) {\n entries[key] = {\n caps: entry.caps,\n blocked: entry.blocked,\n quota: entry.quota,\n };\n }\n }\n return { defaultPolicy: parsed.defaultPolicy, entries };\n }\n } catch {\n // Invalid JSON — fall through to default\n }\n return createState('permissive');\n}\n","/**\n * @kehto/acl — ACL state migration utility.\n *\n * Provides a pure function to migrate persisted ACL state from the old\n * 3-segment composite key format (pubkey:dTag:hash) to the new 2-segment\n * format (dTag:hash) introduced in NIP-5D v0.1.0.\n *\n * No I/O, no side effects. Pure function: takes AclState, returns AclState.\n */\n\nimport type { AclState, AclEntry } from './types.js';\n\n/**\n * Migrate ACL state from old 3-segment key format to new 2-segment key format.\n *\n * Converts entries stored under 'pubkey:dTag:hash' keys to 'dTag:hash' keys.\n * If two old entries map to the same dTag:hash, merges them conservatively:\n * - caps: OR of both bitfields (never removes a granted capability)\n * - blocked: OR of both flags (blocks if either source was blocked)\n * - quota: MAX of both values (keeps the higher allocation)\n *\n * Idempotent: if no 3-segment keys are found, returns the original state\n * unchanged (same object reference).\n *\n * @param state - Current ACL state (may contain old-format entries)\n * @returns Migrated AclState with only 2-segment keys, or the original\n * state unchanged if no migration was needed\n *\n * @example\n * ```ts\n * const oldState = deserialize(localStorage.getItem('napplet:acl') ?? '');\n * const newState = migrateAclState(oldState);\n * if (newState !== oldState) {\n * // Migration occurred — persist the new format\n * localStorage.setItem('napplet:acl', serialize(newState));\n * }\n * ```\n */\nexport function migrateAclState(state: AclState): AclState {\n const newEntries: Record<string, AclEntry> = {};\n let migrated = false;\n\n for (const [key, entry] of Object.entries(state.entries)) {\n const parts = key.split(':');\n if (parts.length === 3) {\n // Old format: pubkey:dTag:hash -> dTag:hash\n const newKey = `${parts[1]}:${parts[2]}`;\n const existing = newEntries[newKey];\n if (existing) {\n // Merge: union caps, preserve block, max quota\n newEntries[newKey] = {\n caps: existing.caps | entry.caps,\n blocked: existing.blocked || entry.blocked,\n quota: Math.max(existing.quota, entry.quota),\n };\n } else {\n newEntries[newKey] = entry;\n }\n migrated = true;\n } else {\n // Already new format or other key — merge if collision with a previously migrated entry\n const existing = newEntries[key];\n if (existing) {\n // Collision: old-format entry was processed first under the same key — merge\n newEntries[key] = {\n caps: existing.caps | entry.caps,\n blocked: existing.blocked || entry.blocked,\n quota: Math.max(existing.quota, entry.quota),\n };\n } else {\n newEntries[key] = entry;\n }\n }\n }\n\n if (!migrated) return state; // No old entries found — return original unchanged\n\n return { defaultPolicy: state.defaultPolicy, entries: newEntries };\n}\n","/**\n * @kehto/acl — NUB domain capability resolution (8-domain canonical).\n *\n * Maps NUB message types (e.g., 'relay.subscribe', 'identity.getProfile') to\n * the capability strings required by sender and recipient. This is the\n * canonical source for \"which capability does this NUB operation require?\"\n * in the @kehto/acl package.\n *\n * Canonical NIP-5D 8 domains: identity, keys, media, notify, relay,\n * storage, ifc, theme. The v1.1 `signer` domain is REMOVED — getPublicKey/\n * getRelays migrated to `identity`; signEvent/nip04/nip44 have no\n * napplet-visible surface (shell handles encryption inside\n * `relay.publishEncrypted`).\n *\n * Zero dependencies. No imports from @napplet/core or any external package.\n *\n * @see packages/acl/src/capabilities.ts for cap string constants + ALL_CAPABILITIES.\n * @see docs/ACL-MIGRATION.md section 2 — Capability Constant to NUB Domain Mapping.\n */\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Minimal message shape used for capability resolution.\n *\n * Compatible with NappletMessage from @napplet/core, but defined here\n * independently to maintain @kehto/acl's zero-dependency constraint.\n *\n * @param type - NUB message type, e.g. 'relay.subscribe', 'identity.getProfile'\n */\nexport interface NubMessage {\n readonly type: string;\n}\n\n/**\n * Result of resolving what capabilities a NUB message requires.\n *\n * | Field | Description |\n * |----------------|----------------------------------------------------------------|\n * | `senderCap` | Capability the sender must have, or null if no check needed |\n * | `recipientCap` | Capability the recipient must have, or null if no check needed |\n *\n * @param senderCap - Capability the sender must have, or null if no ACL gate required\n * @param recipientCap - Capability the recipient must have, or null if no recipient check\n */\nexport interface CapabilityResolution {\n readonly senderCap: string | null;\n readonly recipientCap: string | null;\n}\n\n// ─── Per-domain resolvers ─────────────────────────────────────────────────────\n\n/**\n * `relay.*` — split publish / publishEncrypted / read actions.\n *\n * - `publish` → sender `relay:write`, recipient `relay:read`.\n * - `publishEncrypted` → sender `relay:write`, recipient `null` (the shell\n * handles encryption internally; no napplet-visible recipient ACL check).\n * - `subscribe` / `query` / `close` (and `.result` / `event` / `eose` /\n * `closed` / `publish.result` / `publishEncrypted.result`) → sender\n * `relay:read`, recipient `null`.\n */\nfunction relayMap(action: string): CapabilityResolution {\n if (action === 'publish') return { senderCap: 'relay:write', recipientCap: 'relay:read' };\n if (action === 'publishEncrypted') return { senderCap: 'relay:write', recipientCap: null };\n return { senderCap: 'relay:read', recipientCap: null };\n}\n\n/**\n * `identity.*` — split shell-public reads from gated profile reads.\n *\n * - `getPublicKey` / `getRelays` → `null`/`null` (shell-public info).\n * - `getProfile` / `getFollows` / `getList` / `getZaps` / `getMutes` /\n * `getBlocked` / `getBadges` (and any other identity read) → sender\n * `identity:read`, recipient `null`.\n */\nfunction identityMap(action: string): CapabilityResolution {\n if (action === 'getPublicKey' || action === 'getRelays') {\n return { senderCap: null, recipientCap: null };\n }\n return { senderCap: 'identity:read', recipientCap: null };\n}\n\n/**\n * `keys.*` — split forwarding from binding lifecycle.\n *\n * - `forward` / `action` → `keys:forward`.\n * - `registerAction` / `unregisterAction` / `bindings` → `keys:bind`.\n */\nfunction keysMap(action: string): CapabilityResolution {\n if (action === 'forward' || action === 'action') {\n return { senderCap: 'keys:forward', recipientCap: null };\n }\n return { senderCap: 'keys:bind', recipientCap: null };\n}\n\n/**\n * `notify.*` — split channel/permission registration from send/interaction.\n *\n * - `channel.register` / `permission.request` / `permission.result` → `notify:channel`.\n * - `send` / `dismiss` / `badge` / `send.result` / `action` / `clicked` /\n * `dismissed` / `controls` (and any other notify action) → `notify:send`.\n */\nfunction notifyMap(action: string): CapabilityResolution {\n if (\n action === 'channel.register' ||\n action === 'permission.request' ||\n action === 'permission.result'\n ) {\n return { senderCap: 'notify:channel', recipientCap: null };\n }\n return { senderCap: 'notify:send', recipientCap: null };\n}\n\n/**\n * `storage.*` — narrowed to the canonical 4 actions (get/keys/set/remove).\n *\n * - `get` / `keys` → `state:read`.\n * - `set` / `remove` → `state:write`.\n * - anything else (incl. the removed `clear`) → `null`/`null`. The runtime\n * storage handler rejects non-canonical actions before ACL resolution so\n * napplets see the explicit rejection rather than a misleading cap denial.\n */\nfunction storageMap(action: string): CapabilityResolution {\n if (action === 'get' || action === 'keys') return { senderCap: 'state:read', recipientCap: null };\n if (action === 'set' || action === 'remove') return { senderCap: 'state:write', recipientCap: null };\n return { senderCap: null, recipientCap: null };\n}\n\n/**\n * `ifc.*` — topic + channel sub-protocol.\n *\n * - Write actions (`emit`, `channel.emit`, `channel.broadcast`) → sender\n * `relay:write`, recipient `relay:read`. Semantically equivalent to relay\n * publish: point-to-point or fan-out writes gate on relay-write at wire\n * level even though channel membership ACL is enforced at `channel.open`.\n * - Read / control actions (`subscribe`, `unsubscribe`, `channel.open`,\n * `channel.list`, `channel.close`) → sender\n * `relay:read`, recipient `null`. Channel open-time ACL semantics: the\n * caller must already hold `relay:read`, and channel membership is\n * recorded by the ifc handler.\n */\nfunction ifcMap(action: string): CapabilityResolution {\n if (action === 'emit' || action === 'channel.emit' || action === 'channel.broadcast') {\n return { senderCap: 'relay:write', recipientCap: 'relay:read' };\n }\n return { senderCap: 'relay:read', recipientCap: null };\n}\n\n/**\n * `theme.*` — napplet read gate vs shell-initiated push.\n *\n * - `get` / `get.result` (and any other napplet-originated query) →\n * sender `theme:read`, recipient `null`.\n * - `changed` (shell → napplet push) →\n * sender `null`, recipient `theme:read`. The push is gated against the\n * receiving napplet's cap so a napplet without `theme:read` never sees\n * the update.\n *\n * Note: theme's runtime/service wiring lands in Phase 13. The ACL gate is\n * defined here in Phase 12 so the cap surface is canonical ahead of the\n * runtime work.\n */\nfunction themeMap(action: string): CapabilityResolution {\n if (action === 'changed') return { senderCap: null, recipientCap: 'theme:read' };\n return { senderCap: 'theme:read', recipientCap: null };\n}\n\n// ─── Resolution ───────────────────────────────────────────────────────────────\n\n/**\n * Resolve the capabilities required by a NUB message.\n *\n * Splits `msg.type` on '.' to obtain `[domain, action]`, then dispatches to\n * a per-domain mapper. Unknown domains return `null/null` (silently ignored).\n *\n * **NUB domain mapping table (8 canonical domains):**\n *\n * | Domain | Action(s) | senderCap | recipientCap |\n * |------------|--------------------------------------------------------------|-----------------|---------------|\n * | `relay` | `subscribe`, `query`, `close`, results/pushes | `relay:read` | `null` |\n * | `relay` | `publish` | `relay:write` | `relay:read` |\n * | `relay` | `publishEncrypted` | `relay:write` | `null` |\n * | `identity` | `getPublicKey`, `getRelays` | `null` | `null` |\n * | `identity` | `getProfile/getFollows/getList/getZaps/getMutes/...` | `identity:read` | `null` |\n * | `keys` | `forward`, `action` | `keys:forward` | `null` |\n * | `keys` | `registerAction`, `unregisterAction`, `bindings` | `keys:bind` | `null` |\n * | `media` | any | `media:control` | `null` |\n * | `notify` | `channel.register`, `permission.request`, `permission.result` | `notify:channel`| `null` |\n * | `notify` | `send`, `dismiss`, `badge`, `clicked`, `action`, ... | `notify:send` | `null` |\n * | `storage` | `get`, `keys` | `state:read` | `null` |\n * | `storage` | `set`, `remove` | `state:write` | `null` |\n * | `storage` | any other (incl. removed `clear`) | `null` | `null` |\n * | `ifc` | `emit`, `channel.emit`, `channel.broadcast` | `relay:write` | `relay:read` |\n * | `ifc` | `subscribe`, `unsubscribe`, `channel.open/list/close` | `relay:read` | `null` |\n * | `theme` | `get`, `get.result` | `theme:read` | `null` |\n * | `theme` | `changed` (shell → napplet push) | `null` | `theme:read` |\n * | unknown | any | `null` | `null` |\n *\n * The `signer` domain is REMOVED — signer messages fall through to the\n * default null/null branch. `getPublicKey`/`getRelays` migrated to\n * `identity`; napplet-visible signing does not exist in NIP-5D (shell\n * signs internally for `relay.publishEncrypted`).\n *\n * @param msg - Message with a `type` field in NUB format (e.g., 'relay.subscribe')\n * @returns CapabilityResolution with senderCap and recipientCap (each may be null)\n *\n * @example\n * ```ts\n * resolveCapabilitiesNub({ type: 'relay.subscribe' })\n * // => { senderCap: 'relay:read', recipientCap: null }\n *\n * resolveCapabilitiesNub({ type: 'relay.publishEncrypted' })\n * // => { senderCap: 'relay:write', recipientCap: null }\n *\n * resolveCapabilitiesNub({ type: 'identity.getProfile' })\n * // => { senderCap: 'identity:read', recipientCap: null }\n *\n * resolveCapabilitiesNub({ type: 'keys.forward' })\n * // => { senderCap: 'keys:forward', recipientCap: null }\n *\n * resolveCapabilitiesNub({ type: 'ifc.channel.broadcast' })\n * // => { senderCap: 'relay:write', recipientCap: 'relay:read' }\n *\n * resolveCapabilitiesNub({ type: 'theme.changed' })\n * // => { senderCap: null, recipientCap: 'theme:read' }\n *\n * resolveCapabilitiesNub({ type: 'signer.signEvent' })\n * // => { senderCap: null, recipientCap: null } // domain removed\n * ```\n */\nexport function resolveCapabilitiesNub(msg: NubMessage): CapabilityResolution {\n const dotIdx = msg.type.indexOf('.');\n if (dotIdx === -1) return { senderCap: null, recipientCap: null };\n const domain = msg.type.slice(0, dotIdx);\n const action = msg.type.slice(dotIdx + 1);\n\n switch (domain) {\n case 'relay': return relayMap(action);\n case 'identity': return identityMap(action);\n case 'keys': return keysMap(action);\n case 'media': return { senderCap: 'media:control', recipientCap: null };\n case 'notify': return notifyMap(action);\n case 'storage': return storageMap(action);\n case 'ifc': return ifcMap(action);\n case 'theme': return themeMap(action);\n default: return { senderCap: null, recipientCap: null };\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@kehto/acl",
3
+ "version": "0.1.0",
4
+ "description": "Pure, WASM-ready ACL module for the napplet protocol \u2014 zero dependencies, zero side effects",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./capabilities": {
15
+ "types": "./dist/capabilities.d.ts",
16
+ "import": "./dist/capabilities.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "sideEffects": false,
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {},
27
+ "peerDependencies": {
28
+ "@napplet/core": "^0.2.1",
29
+ "@napplet/nub-identity": "^0.2.1",
30
+ "@napplet/nub-ifc": "^0.2.1",
31
+ "@napplet/nub-keys": "^0.2.1",
32
+ "@napplet/nub-media": "^0.2.1",
33
+ "@napplet/nub-notify": "^0.2.1",
34
+ "@napplet/nub-relay": "^0.2.1",
35
+ "@napplet/nub-storage": "^0.2.1",
36
+ "@napplet/nub-theme": "^0.2.1"
37
+ },
38
+ "devDependencies": {
39
+ "@napplet/core": "^0.2.1",
40
+ "@napplet/nub-identity": "^0.2.1",
41
+ "@napplet/nub-ifc": "^0.2.1",
42
+ "@napplet/nub-keys": "^0.2.1",
43
+ "@napplet/nub-media": "^0.2.1",
44
+ "@napplet/nub-notify": "^0.2.1",
45
+ "@napplet/nub-relay": "^0.2.1",
46
+ "@napplet/nub-storage": "^0.2.1",
47
+ "@napplet/nub-theme": "^0.2.1",
48
+ "tsup": "^8.5.0",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "type-check": "tsc --noEmit",
54
+ "test:unit": "vitest run --config ../../vitest.config.ts"
55
+ },
56
+ "license": "MIT",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/kehto/runtime.git",
60
+ "directory": "packages/acl"
61
+ },
62
+ "keywords": [
63
+ "nostr",
64
+ "napplet",
65
+ "kehto",
66
+ "acl",
67
+ "capability"
68
+ ]
69
+ }