@kehto/acl 0.2.0 → 0.5.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 +19 -14
- package/dist/capabilities.d.ts +10 -14
- package/dist/capabilities.js +9 -1
- package/dist/{chunk-7M23OTGP.js → chunk-T3LIC4XI.js} +19 -3
- package/dist/chunk-T3LIC4XI.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +34 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -21
- package/dist/chunk-7M23OTGP.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Pure, WASM-ready ACL module for the napplet protocol — zero dependencies, zero side effects.
|
|
4
4
|
|
|
5
|
+
> **Alpha status:** Kehto is an early runtime implementation for a draft NIP-5D
|
|
6
|
+
> protocol. NUB contracts and capability names are not final; treat this package
|
|
7
|
+
> as current implementation guidance, not as a stable protocol guarantee.
|
|
8
|
+
|
|
5
9
|
## Install
|
|
6
10
|
|
|
7
11
|
```bash
|
|
@@ -10,14 +14,14 @@ pnpm add @kehto/acl
|
|
|
10
14
|
|
|
11
15
|
## Overview
|
|
12
16
|
|
|
13
|
-
`@kehto/acl` is
|
|
17
|
+
`@kehto/acl` is Kehto's access-control core. It owns an immutable `AclState` keyed on the current NIP-5D 2-segment identity `(dTag, hash)` shape (the earlier `(pubkey, dTag, hash)` triple is dropped; `migrateAclState` ships for legacy persistence readers).
|
|
14
18
|
|
|
15
19
|
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
20
|
|
|
17
21
|
The module exposes two parallel capability surfaces:
|
|
18
22
|
|
|
19
23
|
- **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
|
-
- **
|
|
24
|
+
- **Current draft NIP-5D 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 earlier `sign:event`/`sign:nip04`/`sign:nip44` entries were intentionally removed — the current NIP-5D draft does not expose napplet-visible signing.
|
|
21
25
|
|
|
22
26
|
## Quick Start
|
|
23
27
|
|
|
@@ -37,7 +41,7 @@ let state = createState('restrictive');
|
|
|
37
41
|
|
|
38
42
|
const id = { dTag: 'chat', hash: 'ff00aa11' };
|
|
39
43
|
|
|
40
|
-
// 2. Grant the two
|
|
44
|
+
// 2. Grant the two current draft capabilities this napplet needs.
|
|
41
45
|
state = grant(state, id, CAP_RELAY_WRITE);
|
|
42
46
|
state = grant(state, id, CAP_NOTIFY_SEND);
|
|
43
47
|
|
|
@@ -68,29 +72,30 @@ check(state, id, CAP_RELAY_WRITE); // true (restored)
|
|
|
68
72
|
- `CAP_STATE_READ`, `CAP_STATE_WRITE`, `CAP_ALL`, `CAP_NONE`
|
|
69
73
|
- `DEFAULT_QUOTA`
|
|
70
74
|
|
|
71
|
-
### Constants —
|
|
75
|
+
### Constants — current draft NIP-5D capability strings
|
|
72
76
|
- `ALL_CAPABILITIES` — readonly tuple of every recognized capability string
|
|
73
77
|
- `CAP_IDENTITY_READ`, `CAP_KEYS_BIND`, `CAP_KEYS_FORWARD`
|
|
74
78
|
- `CAP_MEDIA_CONTROL`, `CAP_NOTIFY_SEND`, `CAP_NOTIFY_CHANNEL`, `CAP_THEME_READ`
|
|
75
79
|
|
|
76
80
|
### State mutations
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
81
|
+
- `createState` — create an empty AclState
|
|
82
|
+
- `grant`, `revoke` — add/remove capability bits
|
|
83
|
+
- `block`, `unblock` — toggle the block flag
|
|
84
|
+
- `setQuota`, `getQuota` — per-identity state storage quota
|
|
85
|
+
- `serialize`, `deserialize` — JSON round-trip for persistence
|
|
82
86
|
|
|
83
87
|
### Capability resolution
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
88
|
+
- `check` — evaluate identity + capability against state
|
|
89
|
+
- `toKey` — compute the `dTag:hash` composite key
|
|
90
|
+
- `resolveCapabilitiesNub` — map a NIP-5D NUB envelope type to the required sender/recipient capabilities across the current supported domains
|
|
87
91
|
|
|
88
92
|
### Migration
|
|
89
|
-
-
|
|
93
|
+
- `migrateAclState` — 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
94
|
|
|
91
95
|
## API Reference
|
|
92
96
|
|
|
93
|
-
Full
|
|
97
|
+
Full package docs: [`docs/packages/acl.md`](../../docs/packages/acl.md).
|
|
98
|
+
Generated API module: `docs/api/modules/_kehto_acl.html` (run `pnpm docs:api`).
|
|
94
99
|
|
|
95
100
|
## License
|
|
96
101
|
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
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
1
|
/**
|
|
14
2
|
* All capability strings recognized by @kehto/acl.
|
|
15
3
|
*
|
|
@@ -19,11 +7,13 @@
|
|
|
19
7
|
* napplet-visible signing exists in canonical NIP-5D; signing flows
|
|
20
8
|
* through shell-internal `relay.publishEncrypted` instead.
|
|
21
9
|
*/
|
|
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"];
|
|
10
|
+
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", "config:read", "resource:fetch", "identity:decrypt", "cvm:call"];
|
|
23
11
|
/** Union of every capability string in ALL_CAPABILITIES. */
|
|
24
12
|
type Capability = typeof ALL_CAPABILITIES[number];
|
|
25
13
|
/** identity.getProfile/getFollows/getList/getZaps/getMutes/getBlocked/getBadges */
|
|
26
14
|
declare const CAP_IDENTITY_READ: "identity:read";
|
|
15
|
+
/** identity.decrypt (class-1 only; shell-mediated decrypt) */
|
|
16
|
+
declare const CAP_IDENTITY_DECRYPT: "identity:decrypt";
|
|
27
17
|
/** keys.registerAction / keys.unregisterAction / keys.bindings */
|
|
28
18
|
declare const CAP_KEYS_BIND: "keys:bind";
|
|
29
19
|
/** keys.forward / keys.action */
|
|
@@ -36,5 +26,11 @@ declare const CAP_NOTIFY_SEND: "notify:send";
|
|
|
36
26
|
declare const CAP_NOTIFY_CHANNEL: "notify:channel";
|
|
37
27
|
/** theme.get / theme.changed */
|
|
38
28
|
declare const CAP_THEME_READ: "theme:read";
|
|
29
|
+
/** config.get / config.subscribe / config.unsubscribe / config.registerSchema / config.openSettings */
|
|
30
|
+
declare const CAP_CONFIG_READ: "config:read";
|
|
31
|
+
/** resource.bytes / resource.cancel (inbound) + resource.bytes.result / resource.bytes.error (outbound) */
|
|
32
|
+
declare const CAP_RESOURCE_FETCH: "resource:fetch";
|
|
33
|
+
/** cvm.discover / cvm.request / cvm.close (inbound) + cvm.*.result / cvm.event (outbound) */
|
|
34
|
+
declare const CAP_CVM_CALL: "cvm:call";
|
|
39
35
|
|
|
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 };
|
|
36
|
+
export { ALL_CAPABILITIES, CAP_CONFIG_READ, CAP_CVM_CALL, CAP_IDENTITY_DECRYPT, CAP_IDENTITY_READ, CAP_KEYS_BIND, CAP_KEYS_FORWARD, CAP_MEDIA_CONTROL, CAP_NOTIFY_CHANNEL, CAP_NOTIFY_SEND, CAP_RESOURCE_FETCH, CAP_THEME_READ, type Capability };
|
package/dist/capabilities.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ALL_CAPABILITIES,
|
|
3
|
+
CAP_CONFIG_READ,
|
|
4
|
+
CAP_CVM_CALL,
|
|
5
|
+
CAP_IDENTITY_DECRYPT,
|
|
3
6
|
CAP_IDENTITY_READ,
|
|
4
7
|
CAP_KEYS_BIND,
|
|
5
8
|
CAP_KEYS_FORWARD,
|
|
6
9
|
CAP_MEDIA_CONTROL,
|
|
7
10
|
CAP_NOTIFY_CHANNEL,
|
|
8
11
|
CAP_NOTIFY_SEND,
|
|
12
|
+
CAP_RESOURCE_FETCH,
|
|
9
13
|
CAP_THEME_READ
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-T3LIC4XI.js";
|
|
11
15
|
export {
|
|
12
16
|
ALL_CAPABILITIES,
|
|
17
|
+
CAP_CONFIG_READ,
|
|
18
|
+
CAP_CVM_CALL,
|
|
19
|
+
CAP_IDENTITY_DECRYPT,
|
|
13
20
|
CAP_IDENTITY_READ,
|
|
14
21
|
CAP_KEYS_BIND,
|
|
15
22
|
CAP_KEYS_FORWARD,
|
|
16
23
|
CAP_MEDIA_CONTROL,
|
|
17
24
|
CAP_NOTIFY_CHANNEL,
|
|
18
25
|
CAP_NOTIFY_SEND,
|
|
26
|
+
CAP_RESOURCE_FETCH,
|
|
19
27
|
CAP_THEME_READ
|
|
20
28
|
};
|
|
21
29
|
//# sourceMappingURL=capabilities.js.map
|
|
@@ -15,24 +15,40 @@ var ALL_CAPABILITIES = [
|
|
|
15
15
|
"media:control",
|
|
16
16
|
"notify:send",
|
|
17
17
|
"notify:channel",
|
|
18
|
-
"theme:read"
|
|
18
|
+
"theme:read",
|
|
19
|
+
// v1.7 Phase 39 — NUB-CONFIG reference service (9th domain):
|
|
20
|
+
"config:read",
|
|
21
|
+
// v1.7 Phase 40 — NUB-RESOURCE reference service (10th domain):
|
|
22
|
+
"resource:fetch",
|
|
23
|
+
// v1.8 Phase 45 — NUB-IDENTITY decrypt gate:
|
|
24
|
+
"identity:decrypt",
|
|
25
|
+
// NAP-CVM — ContextVM bridge (11th domain): call MCP-over-Nostr servers.
|
|
26
|
+
"cvm:call"
|
|
19
27
|
];
|
|
20
28
|
var CAP_IDENTITY_READ = "identity:read";
|
|
29
|
+
var CAP_IDENTITY_DECRYPT = "identity:decrypt";
|
|
21
30
|
var CAP_KEYS_BIND = "keys:bind";
|
|
22
31
|
var CAP_KEYS_FORWARD = "keys:forward";
|
|
23
32
|
var CAP_MEDIA_CONTROL = "media:control";
|
|
24
33
|
var CAP_NOTIFY_SEND = "notify:send";
|
|
25
34
|
var CAP_NOTIFY_CHANNEL = "notify:channel";
|
|
26
35
|
var CAP_THEME_READ = "theme:read";
|
|
36
|
+
var CAP_CONFIG_READ = "config:read";
|
|
37
|
+
var CAP_RESOURCE_FETCH = "resource:fetch";
|
|
38
|
+
var CAP_CVM_CALL = "cvm:call";
|
|
27
39
|
|
|
28
40
|
export {
|
|
29
41
|
ALL_CAPABILITIES,
|
|
30
42
|
CAP_IDENTITY_READ,
|
|
43
|
+
CAP_IDENTITY_DECRYPT,
|
|
31
44
|
CAP_KEYS_BIND,
|
|
32
45
|
CAP_KEYS_FORWARD,
|
|
33
46
|
CAP_MEDIA_CONTROL,
|
|
34
47
|
CAP_NOTIFY_SEND,
|
|
35
48
|
CAP_NOTIFY_CHANNEL,
|
|
36
|
-
CAP_THEME_READ
|
|
49
|
+
CAP_THEME_READ,
|
|
50
|
+
CAP_CONFIG_READ,
|
|
51
|
+
CAP_RESOURCE_FETCH,
|
|
52
|
+
CAP_CVM_CALL
|
|
37
53
|
};
|
|
38
|
-
//# sourceMappingURL=chunk-
|
|
54
|
+
//# sourceMappingURL=chunk-T3LIC4XI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/capabilities.ts"],"sourcesContent":["\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 // v1.7 Phase 39 — NUB-CONFIG reference service (9th domain):\n 'config:read',\n // v1.7 Phase 40 — NUB-RESOURCE reference service (10th domain):\n 'resource:fetch',\n // v1.8 Phase 45 — NUB-IDENTITY decrypt gate:\n 'identity:decrypt',\n // NAP-CVM — ContextVM bridge (11th domain): call MCP-over-Nostr servers.\n 'cvm:call',\n] as const;\n\n/** Union of every capability string in ALL_CAPABILITIES. */\nexport type Capability = typeof ALL_CAPABILITIES[number];\n\n/** identity.getProfile/getFollows/getList/getZaps/getMutes/getBlocked/getBadges */\nexport const CAP_IDENTITY_READ = 'identity:read' as const;\n/** identity.decrypt (class-1 only; shell-mediated decrypt) */\nexport const CAP_IDENTITY_DECRYPT = 'identity:decrypt' 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/** config.get / config.subscribe / config.unsubscribe / config.registerSchema / config.openSettings */\nexport const CAP_CONFIG_READ = 'config:read' as const;\n/** resource.bytes / resource.cancel (inbound) + resource.bytes.result / resource.bytes.error (outbound) */\nexport const CAP_RESOURCE_FETCH = 'resource:fetch' as const;\n/** cvm.discover / cvm.request / cvm.close (inbound) + cvm.*.result / cvm.event (outbound) */\nexport const CAP_CVM_CALL = 'cvm:call' as const;\n"],"mappings":";AAUO,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;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACF;AAMO,IAAM,oBAAsB;AAE5B,IAAM,uBAAuB;AAE7B,IAAM,gBAAsB;AAE5B,IAAM,mBAAsB;AAE5B,IAAM,oBAAsB;AAE5B,IAAM,kBAAsB;AAE5B,IAAM,qBAAsB;AAE5B,IAAM,iBAAsB;AAE5B,IAAM,kBAAsB;AAE5B,IAAM,qBAAsB;AAE5B,IAAM,eAAsB;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
1
|
+
export { ALL_CAPABILITIES, CAP_CVM_CALL, CAP_IDENTITY_DECRYPT, 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
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @kehto/acl — Type definitions and capability bit constants.
|
|
@@ -323,7 +323,7 @@ declare function deserialize(json: string): AclState;
|
|
|
323
323
|
declare function migrateAclState(state: AclState): AclState;
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
|
-
* @kehto/acl — NUB domain capability resolution (
|
|
326
|
+
* @kehto/acl — NUB domain capability resolution (10-domain: 8 canonical + config + resource).
|
|
327
327
|
*
|
|
328
328
|
* Maps NUB message types (e.g., 'relay.subscribe', 'identity.getProfile') to
|
|
329
329
|
* the capability strings required by sender and recipient. This is the
|
|
@@ -331,9 +331,10 @@ declare function migrateAclState(state: AclState): AclState;
|
|
|
331
331
|
* in the @kehto/acl package.
|
|
332
332
|
*
|
|
333
333
|
* Canonical NIP-5D 8 domains: identity, keys, media, notify, relay,
|
|
334
|
-
* storage, ifc, theme.
|
|
335
|
-
*
|
|
336
|
-
*
|
|
334
|
+
* storage, ifc, theme. Extended in v1.7 with: config (Phase 39, 9th domain),
|
|
335
|
+
* resource (Phase 40, 10th domain). The v1.1 `signer` domain is REMOVED —
|
|
336
|
+
* getPublicKey/getRelays migrated to `identity`; signEvent/nip04/nip44 have
|
|
337
|
+
* no napplet-visible surface (shell handles encryption inside
|
|
337
338
|
* `relay.publishEncrypted`).
|
|
338
339
|
*
|
|
339
340
|
* Zero dependencies. No imports from @napplet/core or any external package.
|
|
@@ -381,6 +382,7 @@ interface CapabilityResolution {
|
|
|
381
382
|
* | `relay` | `publish` | `relay:write` | `relay:read` |
|
|
382
383
|
* | `relay` | `publishEncrypted` | `relay:write` | `null` |
|
|
383
384
|
* | `identity` | `getPublicKey`, `getRelays` | `null` | `null` |
|
|
385
|
+
* | `identity` | `decrypt` | `identity:decrypt` | `null` |
|
|
384
386
|
* | `identity` | `getProfile/getFollows/getList/getZaps/getMutes/...` | `identity:read` | `null` |
|
|
385
387
|
* | `keys` | `forward`, `action` | `keys:forward` | `null` |
|
|
386
388
|
* | `keys` | `registerAction`, `unregisterAction`, `bindings` | `keys:bind` | `null` |
|
|
@@ -394,6 +396,10 @@ interface CapabilityResolution {
|
|
|
394
396
|
* | `ifc` | `subscribe`, `unsubscribe`, `channel.open/list/close` | `relay:read` | `null` |
|
|
395
397
|
* | `theme` | `get`, `get.result` | `theme:read` | `null` |
|
|
396
398
|
* | `theme` | `changed` (shell → napplet push) | `null` | `theme:read` |
|
|
399
|
+
* | `config` | `get`, `subscribe`, `unsubscribe`, `registerSchema`, `openSettings` | `config:read` | `null` |
|
|
400
|
+
* | `config` | `values`, `registerSchema.result`, `schemaError` (shell → napplet pushes) | `null` | `config:read` |
|
|
401
|
+
* | `resource` | `bytes`, `cancel` (napplet → shell requests) | `resource:fetch`| `null` |
|
|
402
|
+
* | `resource` | `bytes.result`, `bytes.error` (shell → napplet pushes) | `null` | `resource:fetch` |
|
|
397
403
|
* | unknown | any | `null` | `null` |
|
|
398
404
|
*
|
|
399
405
|
* The `signer` domain is REMOVED — signer messages fall through to the
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ALL_CAPABILITIES,
|
|
3
|
+
CAP_CVM_CALL,
|
|
4
|
+
CAP_IDENTITY_DECRYPT,
|
|
3
5
|
CAP_IDENTITY_READ,
|
|
4
6
|
CAP_KEYS_BIND,
|
|
5
7
|
CAP_KEYS_FORWARD,
|
|
@@ -7,7 +9,7 @@ import {
|
|
|
7
9
|
CAP_NOTIFY_CHANNEL,
|
|
8
10
|
CAP_NOTIFY_SEND,
|
|
9
11
|
CAP_THEME_READ
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-T3LIC4XI.js";
|
|
11
13
|
|
|
12
14
|
// src/types.ts
|
|
13
15
|
var CAP_RELAY_READ = 1 << 0;
|
|
@@ -184,6 +186,9 @@ function identityMap(action) {
|
|
|
184
186
|
if (action === "getPublicKey" || action === "getRelays") {
|
|
185
187
|
return { senderCap: null, recipientCap: null };
|
|
186
188
|
}
|
|
189
|
+
if (action === "decrypt") {
|
|
190
|
+
return { senderCap: "identity:decrypt", recipientCap: null };
|
|
191
|
+
}
|
|
187
192
|
return { senderCap: "identity:read", recipientCap: null };
|
|
188
193
|
}
|
|
189
194
|
function keysMap(action) {
|
|
@@ -209,6 +214,24 @@ function ifcMap(action) {
|
|
|
209
214
|
}
|
|
210
215
|
return { senderCap: "relay:read", recipientCap: null };
|
|
211
216
|
}
|
|
217
|
+
function configMap(action) {
|
|
218
|
+
if (action === "values" || action === "registerSchema.result" || action === "schemaError") {
|
|
219
|
+
return { senderCap: null, recipientCap: "config:read" };
|
|
220
|
+
}
|
|
221
|
+
return { senderCap: "config:read", recipientCap: null };
|
|
222
|
+
}
|
|
223
|
+
function resourceMap(action) {
|
|
224
|
+
if (action === "bytes.result" || action === "bytes.error") {
|
|
225
|
+
return { senderCap: null, recipientCap: "resource:fetch" };
|
|
226
|
+
}
|
|
227
|
+
return { senderCap: "resource:fetch", recipientCap: null };
|
|
228
|
+
}
|
|
229
|
+
function cvmMap(action) {
|
|
230
|
+
if (action === "event" || action.endsWith(".result") || action.endsWith(".error")) {
|
|
231
|
+
return { senderCap: null, recipientCap: "cvm:call" };
|
|
232
|
+
}
|
|
233
|
+
return { senderCap: "cvm:call", recipientCap: null };
|
|
234
|
+
}
|
|
212
235
|
function themeMap(action) {
|
|
213
236
|
if (action === "changed") return { senderCap: null, recipientCap: "theme:read" };
|
|
214
237
|
return { senderCap: "theme:read", recipientCap: null };
|
|
@@ -235,6 +258,14 @@ function resolveCapabilitiesNub(msg) {
|
|
|
235
258
|
return ifcMap(action);
|
|
236
259
|
case "theme":
|
|
237
260
|
return themeMap(action);
|
|
261
|
+
case "config":
|
|
262
|
+
return configMap(action);
|
|
263
|
+
case "resource":
|
|
264
|
+
return resourceMap(action);
|
|
265
|
+
// Phase 40 (RESOURCE-02)
|
|
266
|
+
case "cvm":
|
|
267
|
+
return cvmMap(action);
|
|
268
|
+
// NAP-CVM ContextVM bridge
|
|
238
269
|
default:
|
|
239
270
|
return { senderCap: null, recipientCap: null };
|
|
240
271
|
}
|
|
@@ -244,7 +275,9 @@ export {
|
|
|
244
275
|
CAP_ALL,
|
|
245
276
|
CAP_CACHE_READ,
|
|
246
277
|
CAP_CACHE_WRITE,
|
|
278
|
+
CAP_CVM_CALL,
|
|
247
279
|
CAP_HOTKEY_FORWARD,
|
|
280
|
+
CAP_IDENTITY_DECRYPT,
|
|
248
281
|
CAP_IDENTITY_READ,
|
|
249
282
|
CAP_KEYS_BIND,
|
|
250
283
|
CAP_KEYS_FORWARD,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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":[]}
|
|
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/** 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/**\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/**\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/**\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 { CAP_ALL, DEFAULT_QUOTA, type AclEntry, type AclState, type Identity } 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 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 (10-domain: 8 canonical + config + resource).\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. Extended in v1.7 with: config (Phase 39, 9th domain),\n * resource (Phase 40, 10th domain). The v1.1 `signer` domain is REMOVED —\n * getPublicKey/getRelays migrated to `identity`; signEvent/nip04/nip44 have\n * no 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/**\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/**\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 * - `decrypt` → sender `identity:decrypt` (class-1 only).\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 if (action === 'decrypt') {\n return { senderCap: 'identity:decrypt', 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 * `config.*` — NUB-CONFIG reference service (v1.7 Phase 39 / 9th NUB domain).\n *\n * Asymmetric protocol: napplet reads, shell writes. ALL napplet-originated\n * config messages require `config:read`. Shell→napplet pushes\n * (`config.values`, `config.registerSchema.result`, `config.schemaError`)\n * are gated by the recipient's `config:read` cap.\n *\n * Anti-overlap: NUB-STORAGE remains the general key-value surface\n * (`state:read`/`state:write`). NUB-CONFIG is shell-managed per-napplet\n * configuration only — see CONFIG-04 scope boundary docs.\n */\nfunction configMap(action: string): CapabilityResolution {\n // Shell-originated pushes: recipient gate (napplet must hold config:read to see them).\n if (action === 'values' || action === 'registerSchema.result' || action === 'schemaError') {\n return { senderCap: null, recipientCap: 'config:read' };\n }\n // Napplet-originated requests: sender gate.\n return { senderCap: 'config:read', recipientCap: null };\n}\n\n/**\n * `resource.*` — NUB-RESOURCE authenticated fetch proxy (v1.7 Phase 40 / 10th NUB domain).\n *\n * Asymmetric protocol: napplet initiates fetch requests, shell proxies and responds.\n *\n * - `bytes` / `cancel` (napplet → shell requests) →\n * sender `resource:fetch`, recipient `null`. The napplet must hold\n * `resource:fetch` to issue a bytes request or cancel one.\n * - `bytes.result` / `bytes.error` (shell → napplet pushes) →\n * sender `null`, recipient `resource:fetch`. The napplet must hold\n * `resource:fetch` to receive the result/error push.\n * - Unknown resource.* actions → sender `resource:fetch`, recipient `null`\n * (default sender gate: napplet must hold resource:fetch to send anything\n * in the resource domain).\n */\nfunction resourceMap(action: string): CapabilityResolution {\n // Shell-originated pushes: recipient gate (napplet must hold resource:fetch to see them).\n if (action === 'bytes.result' || action === 'bytes.error') {\n return { senderCap: null, recipientCap: 'resource:fetch' };\n }\n // Napplet-originated requests: sender gate (bytes, cancel, and any unknown).\n return { senderCap: 'resource:fetch', recipientCap: null };\n}\n\n/**\n * `cvm.*` — NAP-CVM ContextVM bridge. Single `cvm:call` cap gates the domain.\n *\n * - `discover` / `request` / `close` (napplet → shell requests) →\n * sender `cvm:call`, recipient `null`. The napplet must hold `cvm:call`\n * to query servers or send MCP messages.\n * - `discover.result` / `request.result` / `close.result` / `event`\n * (shell → napplet pushes) → sender `null`, recipient `cvm:call`. The push\n * is gated against the receiving napplet's cap so a napplet without\n * `cvm:call` never sees CVM results or server-pushed events.\n * - Unknown `cvm.*` actions → sender `cvm:call` (default sender gate).\n */\nfunction cvmMap(action: string): CapabilityResolution {\n // Shell-originated pushes: recipient gate.\n if (action === 'event' || action.endsWith('.result') || action.endsWith('.error')) {\n return { senderCap: null, recipientCap: 'cvm:call' };\n }\n // Napplet-originated requests: sender gate (discover, request, close, unknown).\n return { senderCap: 'cvm:call', 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/**\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` | `decrypt` | `identity:decrypt` | `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 * | `config` | `get`, `subscribe`, `unsubscribe`, `registerSchema`, `openSettings` | `config:read` | `null` |\n * | `config` | `values`, `registerSchema.result`, `schemaError` (shell → napplet pushes) | `null` | `config:read` |\n * | `resource` | `bytes`, `cancel` (napplet → shell requests) | `resource:fetch`| `null` |\n * | `resource` | `bytes.result`, `bytes.error` (shell → napplet pushes) | `null` | `resource:fetch` |\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 case 'config': return configMap(action);\n case 'resource': return resourceMap(action); // Phase 40 (RESOURCE-02)\n case 'cvm': return cvmMap(action); // NAP-CVM ContextVM bridge\n default: return { senderCap: null, recipientCap: null };\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kehto/acl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Pure, WASM-ready ACL module for the napplet protocol — zero dependencies, zero side effects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,33 +25,19 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@napplet/core": "^0.
|
|
29
|
-
"@napplet/nub
|
|
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"
|
|
28
|
+
"@napplet/core": "^0.5.0",
|
|
29
|
+
"@napplet/nub": "^0.5.0"
|
|
37
30
|
},
|
|
38
31
|
"devDependencies": {
|
|
39
|
-
"@napplet/core": "^0.
|
|
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",
|
|
32
|
+
"@napplet/core": "^0.5.0",
|
|
48
33
|
"tsup": "^8.5.0",
|
|
49
|
-
"typescript": "^5.9.3"
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"@napplet/nub": "^0.5.0"
|
|
50
36
|
},
|
|
51
37
|
"license": "MIT",
|
|
52
38
|
"repository": {
|
|
53
39
|
"type": "git",
|
|
54
|
-
"url": "git+https://github.com/kehto/
|
|
40
|
+
"url": "git+https://github.com/kehto/web.git",
|
|
55
41
|
"directory": "packages/acl"
|
|
56
42
|
},
|
|
57
43
|
"keywords": [
|
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|