@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 +97 -0
- package/dist/capabilities.d.ts +40 -0
- package/dist/capabilities.js +21 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/chunk-7M23OTGP.js +38 -0
- package/dist/chunk-7M23OTGP.js.map +1 -0
- package/dist/index.d.ts +433 -0
- package/dist/index.js +278 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|