approval-md 0.2.0 → 0.3.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 +63 -24
- package/SPEC.md +57 -11
- package/dist/src/channels/contract.d.ts +34 -1
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +123 -11
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +99 -26
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +9 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.js +467 -12
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +6 -2
- package/dist/src/cli/help.js +165 -60
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +459 -3
- package/dist/src/cli/hook.js +1062 -114
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/main.js +5 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +115 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.js +174 -9
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest.d.ts +215 -0
- package/dist/src/core/attest.js +317 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +18 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +154 -0
- package/dist/src/core/command-class.js +673 -20
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +86 -1
- package/dist/src/core/gate.js +81 -1
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +1 -1
- package/dist/src/core/harness-version.js +3 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log.d.ts +39 -1
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +10 -0
- package/dist/src/core/policy-explain.js +32 -0
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +41 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +43 -0
- package/dist/src/core/policy-match.js +52 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +52 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +8 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +932 -32
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +3 -1
- package/schema/event.schema.json +538 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +54 -2
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* From an authenticated channel sender to an attested human identity
|
|
3
|
+
* (APRV-324, `design/channel-sender-identity.md`, amended SPEC.md §5.2/§10.3).
|
|
4
|
+
*
|
|
5
|
+
* ## The fact this module is about
|
|
6
|
+
*
|
|
7
|
+
* Before it, every Telegram tap was recorded against the actor the listener
|
|
8
|
+
* process was launched with. `routeCallback` checked `message.chat.id` against
|
|
9
|
+
* the configured chat and never read the update's `from` object at all, so two
|
|
10
|
+
* people in one chat both approved as one name and the log could not tell them
|
|
11
|
+
* apart. GitHub issue #137 asked for the person who tapped; this is the
|
|
12
|
+
* mapping that produces one.
|
|
13
|
+
*
|
|
14
|
+
* ## What is evidence here, and what is not
|
|
15
|
+
*
|
|
16
|
+
* Exactly one field in the whole system is a sender fact worth mapping:
|
|
17
|
+
* Telegram's `callback_query.from.id`, the stable numeric account id the Bot
|
|
18
|
+
* API attributes the tap to. {@link SENDER_CHANNELS} is closed to that one
|
|
19
|
+
* channel for that reason. A web form post authenticates nobody and a CLI
|
|
20
|
+
* process authenticates local machine control, so neither may be given an
|
|
21
|
+
* identity key it cannot support: a `senders` entry for either is a schema
|
|
22
|
+
* violation rather than a mapping nothing backs.
|
|
23
|
+
*
|
|
24
|
+
* Three things are deliberately NOT inputs:
|
|
25
|
+
*
|
|
26
|
+
* - **`from.username`.** Mutable and reusable, so a mapping keyed on it would
|
|
27
|
+
* transfer an identity with a handle. It is never a key and never recorded.
|
|
28
|
+
* - **Anything the message body says.** A callback whose payload or text names
|
|
29
|
+
* a user id is naming it about itself; SPEC.md §11.1 invariant 4 is the rule,
|
|
30
|
+
* and the whole difference between this design and a spoofable one is that
|
|
31
|
+
* the key is the transport's own attribution rather than a claim inside it.
|
|
32
|
+
* - **A sender on a channel with no transport authentication.** See above.
|
|
33
|
+
*
|
|
34
|
+
* And the mapping itself is evidence about an ACCOUNT, not about a person. What
|
|
35
|
+
* binds the account to a human is the operator's assertion, written in
|
|
36
|
+
* `APPROVAL.md`, which is `policy.core` and human-only and inoperative until a
|
|
37
|
+
* human re-attests it. That is why the mapping lives in the policy: it inherits
|
|
38
|
+
* the ceremony that already protects the approver roster it sits inside, and an
|
|
39
|
+
* agent can no more add itself as an approver's sender than as an approver.
|
|
40
|
+
*
|
|
41
|
+
* ## The four modes (design §3.2)
|
|
42
|
+
*
|
|
43
|
+
* {@link resolveSender} is total and pure, and returns one of:
|
|
44
|
+
*
|
|
45
|
+
* 1. `configured` — nothing to resolve against, so the surface keeps the actor
|
|
46
|
+
* it was launched with. This is today's behaviour, and it is what every
|
|
47
|
+
* deployment that never writes a `senders` key stays in forever.
|
|
48
|
+
* 2. `mapped` — the policy maps this sender to exactly one approver, and the
|
|
49
|
+
* decision is recorded as that person whatever the process was launched as.
|
|
50
|
+
* 3. `unmapped` — refuse. Never a fallback to the configured actor, which is
|
|
51
|
+
* today's behaviour dressed as a feature and would let a stranger in the
|
|
52
|
+
* chat approve as the operator.
|
|
53
|
+
* 4. `ambiguous` — two people claiming one account is an operator error, and
|
|
54
|
+
* the runtime does not resolve it by picking. `core/policy-load.ts` refuses
|
|
55
|
+
* such a policy at LOAD time, so this mode is the belt on that brace.
|
|
56
|
+
*
|
|
57
|
+
* ## Why mode 1 is keyed on the policy and not on the transport
|
|
58
|
+
*
|
|
59
|
+
* The channel supplies a sender for every tap once it can see one. If a
|
|
60
|
+
* supplied sender with nothing to map it against were a refusal, the first
|
|
61
|
+
* upgrade of the listener would lock every existing installation out of its own
|
|
62
|
+
* gate. So the question mode 1 asks is whether the POLICY configures a mapping
|
|
63
|
+
* for that channel at all: no approver declaring `senders.<channel>` means the
|
|
64
|
+
* operator has not adopted this, and the decision is attributed by
|
|
65
|
+
* configuration exactly as before. Writing the first mapping for a channel is
|
|
66
|
+
* what turns enforcement on for it, which is the migration `design §6`
|
|
67
|
+
* describes and `tests/sender-identity.test.ts` pins.
|
|
68
|
+
*
|
|
69
|
+
* ## A policy that does not load
|
|
70
|
+
*
|
|
71
|
+
* Fails closed: a supplied sender is refused `sender-unmapped`. A load failure
|
|
72
|
+
* is not "a policy with no mapping", it is a policy the runtime could not read,
|
|
73
|
+
* and inventing an identity from bytes it could not parse is the one thing this
|
|
74
|
+
* module exists to stop. Nothing is stranded by it — the CLI channel supplies
|
|
75
|
+
* no sender, so a terminal can still decide and still repair the file, which is
|
|
76
|
+
* where a `policy.core` edit has to happen anyway.
|
|
77
|
+
*/
|
|
78
|
+
import type { PolicyLoadResult } from "./policy-load.js";
|
|
79
|
+
/**
|
|
80
|
+
* The prefix a KEYED sender mapping wears, in the policy and in the log
|
|
81
|
+
* (APRV-370).
|
|
82
|
+
*
|
|
83
|
+
* ## Why keyed, and not a plain digest
|
|
84
|
+
*
|
|
85
|
+
* The operator raised this on 2026-09-18 while applying APRV-324: a Telegram
|
|
86
|
+
* account id is a short decimal number, and this repository publishes both its
|
|
87
|
+
* policy and its log. Writing the raw id discloses the account once in
|
|
88
|
+
* `APPROVAL.md` and then on every phone decision. It is an identifier rather
|
|
89
|
+
* than a credential and the gate does not depend on its secrecy, so this is a
|
|
90
|
+
* disclosure question rather than a security hole, and the fix has to actually
|
|
91
|
+
* fix it: a plain `sha256:<hex>` of a ten-digit number is not a fix, because
|
|
92
|
+
* the whole space of ten-digit numbers is ten billion digests and a laptop
|
|
93
|
+
* enumerates it in minutes. The operator ruled on 2026-09-19 for the keyed
|
|
94
|
+
* form, which has no such space to enumerate without the key.
|
|
95
|
+
*
|
|
96
|
+
* ## What the key is, and what it is not
|
|
97
|
+
*
|
|
98
|
+
* An operator-held secret in the launch environment, beside the sampling
|
|
99
|
+
* secret, minted by `approval setup sender-key` and named by
|
|
100
|
+
* {@link SENDER_KEY_ENV}. It is NOT an authenticator: nothing about the gate's
|
|
101
|
+
* safety rests on it, and an attacker who learns it learns only which account
|
|
102
|
+
* ids the policy names, which is what the raw form told everybody anyway. It
|
|
103
|
+
* exists so that a published policy and a published log carry a value nobody
|
|
104
|
+
* can walk backwards.
|
|
105
|
+
*/
|
|
106
|
+
export declare const SENDER_HASH_PREFIX = "hmac-sha256:";
|
|
107
|
+
/**
|
|
108
|
+
* The environment variable the sender key is read from.
|
|
109
|
+
*
|
|
110
|
+
* A CONVENTIONAL name rather than one the policy declares, which is the one
|
|
111
|
+
* place this diverges from the sampling secret's shape, and the reason is that
|
|
112
|
+
* the two questions differ. The policy names `audit.sampling_secret_env`
|
|
113
|
+
* because the POLICY decides whether sampling happens at all: a policy naming
|
|
114
|
+
* no variable turns the sampler off, and that is a deliberate control. Here the
|
|
115
|
+
* mapping's own form decides — a value wearing {@link SENDER_HASH_PREFIX} is
|
|
116
|
+
* keyed and a decimal one is not — so the policy already says everything it
|
|
117
|
+
* needs to, and a second declaration would be a second place for one fact to be
|
|
118
|
+
* wrong. Per-instance isolation comes from the env FILE beside the log, which
|
|
119
|
+
* is where the sampling secret gets it too.
|
|
120
|
+
*/
|
|
121
|
+
export declare const SENDER_KEY_ENV = "APPROVAL_SENDER_KEY";
|
|
122
|
+
/** Is this mapping value (or recorded id) the keyed form? */
|
|
123
|
+
export declare function isHashedSenderId(value: string): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* The keyed digest of one observed id: what a keyed policy carries and what a
|
|
126
|
+
* keyed record records.
|
|
127
|
+
*
|
|
128
|
+
* HMAC-SHA-256 under the operator's key, over the id as the transport reported
|
|
129
|
+
* it, hex, prefixed. Computed the way `core/sampler.ts` computes its selection
|
|
130
|
+
* value — `createHmac("sha256", key).update(value, "utf8")` — so this runtime
|
|
131
|
+
* has one keyed-digest idiom rather than two that could drift.
|
|
132
|
+
*
|
|
133
|
+
* The prefix is part of the value on purpose. It is what tells a reader of a
|
|
134
|
+
* policy or a log which form they are looking at without a second field to
|
|
135
|
+
* consult, and it is what makes a keyed entry and a raw entry unable to
|
|
136
|
+
* collide: a raw Telegram id is decimal digits and can never be this string.
|
|
137
|
+
*/
|
|
138
|
+
export declare function hashedSenderId(key: string, id: string): string;
|
|
139
|
+
/** The sender key in this environment, or `null` when it is unset or empty. */
|
|
140
|
+
export declare function senderKeyFrom(env?: NodeJS.ProcessEnv): string | null;
|
|
141
|
+
/**
|
|
142
|
+
* The channels whose transport attributes a gesture to an account the operator
|
|
143
|
+
* can map (design §2).
|
|
144
|
+
*
|
|
145
|
+
* Closed, and short on purpose. `telegram` is here because the Bot API reports
|
|
146
|
+
* `callback_query.from.id`, a stable numeric account id the sender cannot
|
|
147
|
+
* choose. `web` and `cli` are absent because neither authenticates a person:
|
|
148
|
+
* the web page takes an unauthenticated form post and the CLI takes whoever
|
|
149
|
+
* controls the process. A policy that named either would be asserting a binding
|
|
150
|
+
* the runtime cannot check, so the schema refuses the key rather than carrying
|
|
151
|
+
* a mapping that reads like identity.
|
|
152
|
+
*/
|
|
153
|
+
export declare const SENDER_CHANNELS: readonly ["telegram"];
|
|
154
|
+
export type SenderChannel = (typeof SENDER_CHANNELS)[number];
|
|
155
|
+
/** Is `name` a channel whose transport can attribute a gesture (§2)? */
|
|
156
|
+
export declare function isSenderChannel(name: string): name is SenderChannel;
|
|
157
|
+
/**
|
|
158
|
+
* One observed sender: the channel that saw it, and the id that channel's
|
|
159
|
+
* transport attributed the gesture to.
|
|
160
|
+
*
|
|
161
|
+
* `id` is always the transport's own attribution. For Telegram it is
|
|
162
|
+
* `String(callback_query.from.id)` and nothing else — not a username, not a
|
|
163
|
+
* chat id, and nothing read out of the message.
|
|
164
|
+
*/
|
|
165
|
+
export interface ChannelSender {
|
|
166
|
+
/** The surface that observed it: `telegram`. */
|
|
167
|
+
channel: string;
|
|
168
|
+
/** The transport's stable account id, as a string. */
|
|
169
|
+
id: string;
|
|
170
|
+
}
|
|
171
|
+
/** Where a decision's actor came from, recorded on the decision (design §4). */
|
|
172
|
+
export declare const SENDER_SOURCES: readonly ["policy"];
|
|
173
|
+
export type SenderSource = (typeof SENDER_SOURCES)[number];
|
|
174
|
+
/**
|
|
175
|
+
* Refusals that belong to the decision SURFACE rather than to the gate
|
|
176
|
+
* (§11.1 invariant 6, amended SPEC.md §11.2).
|
|
177
|
+
*
|
|
178
|
+
* Its own union, deliberately. `GATE_REFUSAL_CODES` is documented as every way
|
|
179
|
+
* `approval register|request|decide|withdraw|expire` can refuse, and `decide`
|
|
180
|
+
* cannot emit either of these: the resolution happens before it is called, and
|
|
181
|
+
* a second implementation whose gate emitted one would be describing a
|
|
182
|
+
* different boundary. Both codes are stable public API and both are pinned by
|
|
183
|
+
* `conformance/vectors/refusal-unions.v1.json`.
|
|
184
|
+
*/
|
|
185
|
+
export declare const CHANNEL_DECISION_REFUSAL_CODES: readonly [
|
|
186
|
+
/**
|
|
187
|
+
* A sender arrived on a channel the policy maps senders for, and it maps this
|
|
188
|
+
* one to nobody — or the policy could not be loaded at all. Nothing is
|
|
189
|
+
* decided; one `audit.decision_refused` records the observed id.
|
|
190
|
+
*/
|
|
191
|
+
"sender-unmapped",
|
|
192
|
+
/**
|
|
193
|
+
* One sender id is claimed by two approvers. `core/policy-load.ts` refuses
|
|
194
|
+
* such a policy outright, so reaching this at decision time means a caller
|
|
195
|
+
* supplied a policy that never passed a load; either way the runtime does not
|
|
196
|
+
* pick.
|
|
197
|
+
*/
|
|
198
|
+
"sender-ambiguous",
|
|
199
|
+
/**
|
|
200
|
+
* The policy maps this channel's senders in the KEYED form and no sender key
|
|
201
|
+
* resolves in this process (APRV-370).
|
|
202
|
+
*
|
|
203
|
+
* Its own code rather than a `sender-unmapped`, because the two say opposite
|
|
204
|
+
* things and want opposite repairs. Unmapped says the policy does not name
|
|
205
|
+
* this account: the operator looks at the account and decides whether to add
|
|
206
|
+
* it. This says the runtime could not evaluate the mapping AT ALL, for every
|
|
207
|
+
* account, and the repair is {@link SENDER_KEY_ENV} in the listener's
|
|
208
|
+
* environment. A caller that could not tell them apart would send an operator
|
|
209
|
+
* looking for an intruder when what happened is that a process started
|
|
210
|
+
* without its key.
|
|
211
|
+
*
|
|
212
|
+
* It refuses the WHOLE channel, not only the keyed entries, and that is the
|
|
213
|
+
* strict reading rather than an accident. Without the key the runtime cannot
|
|
214
|
+
* compute any digest, so it cannot check whether the observed account is also
|
|
215
|
+
* claimed by a keyed approver, which means it cannot run the ambiguity check
|
|
216
|
+
* the mapping's safety rests on. Matching a raw entry while half the roster
|
|
217
|
+
* is unreadable would be resolving an ambiguity by not looking at it
|
|
218
|
+
* (SPEC.md §11.1: ambiguity resolves to the stricter path, always). There is
|
|
219
|
+
* deliberately no fallback to the raw comparison.
|
|
220
|
+
*/
|
|
221
|
+
"sender-key-unavailable",
|
|
222
|
+
/**
|
|
223
|
+
* An attestation tap that would decide WHO MAY DECIDE, from a phone, under a
|
|
224
|
+
* policy that cannot answer who is tapping.
|
|
225
|
+
*
|
|
226
|
+
* The attestation ceremony is the one act whose subject can be the sender
|
|
227
|
+
* mapping itself, so resolving it against the file being attested would let
|
|
228
|
+
* whoever edited that file name themselves as the approver of their own
|
|
229
|
+
* edit. It is resolved against the policy IN FORCE instead, and this code is
|
|
230
|
+
* what fires when that policy cannot answer: its bytes are not recoverable,
|
|
231
|
+
* or it maps no sender for this channel while the amendment changes the
|
|
232
|
+
* mapping. The repair is a terminal, which supplies no sender and is where a
|
|
233
|
+
* `policy.core` edit happens anyway.
|
|
234
|
+
*/
|
|
235
|
+
"attest-requires-terminal"];
|
|
236
|
+
export type ChannelDecisionRefusalCode = (typeof CHANNEL_DECISION_REFUSAL_CODES)[number];
|
|
237
|
+
/** Is `code` one of this union's members? Used where a code arrives as a string. */
|
|
238
|
+
export declare function isChannelDecisionRefusalCode(code: string): code is ChannelDecisionRefusalCode;
|
|
239
|
+
/** The outcome of resolving one observed sender against one policy. */
|
|
240
|
+
export type SenderResolution = {
|
|
241
|
+
/** Nothing to resolve against; the surface keeps its configured actor. */
|
|
242
|
+
kind: "configured";
|
|
243
|
+
/** Why: no sender was observed, or the policy maps none for this channel. */
|
|
244
|
+
reason: "no-sender" | "channel-unmapped";
|
|
245
|
+
} | {
|
|
246
|
+
kind: "mapped";
|
|
247
|
+
approver: string;
|
|
248
|
+
actor: string;
|
|
249
|
+
source: SenderSource;
|
|
250
|
+
/** The sender AS THE RECORD CARRIES IT: raw, or the keyed digest. */
|
|
251
|
+
recorded: RecordedSender;
|
|
252
|
+
} | {
|
|
253
|
+
kind: "unmapped";
|
|
254
|
+
sender: RecordedSender;
|
|
255
|
+
message: string;
|
|
256
|
+
} | {
|
|
257
|
+
kind: "ambiguous";
|
|
258
|
+
sender: RecordedSender;
|
|
259
|
+
approvers: string[];
|
|
260
|
+
message: string;
|
|
261
|
+
} | {
|
|
262
|
+
kind: "key-unavailable";
|
|
263
|
+
sender: ChannelSender;
|
|
264
|
+
message: string;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* A sender as a RECORD carries it (APRV-370).
|
|
268
|
+
*
|
|
269
|
+
* Under a raw mapping this is `{channel, id}` and byte-identical to what every
|
|
270
|
+
* build since APRV-324 wrote. Under a keyed mapping `id` is the
|
|
271
|
+
* {@link hashedSenderId} form and `hashed` is `true`.
|
|
272
|
+
*
|
|
273
|
+
* `id` carries the WHOLE `hmac-sha256:<hex>` string rather than the bare hex,
|
|
274
|
+
* deliberately: that is exactly the string the policy carries, so an operator
|
|
275
|
+
* reading a refusal off the log has a line they can paste into `senders`
|
|
276
|
+
* without transforming it, and a reader correlating a log to a policy can grep
|
|
277
|
+
* one for the other. The `hashed` flag is not a second source of truth for the
|
|
278
|
+
* same fact — the schema pins `id` to the digest shape whenever it is present,
|
|
279
|
+
* so the two cannot disagree — it is the field anything machine-readable
|
|
280
|
+
* branches on without parsing a string.
|
|
281
|
+
*
|
|
282
|
+
* `hashed` is `true` or absent, never `false`. A raw record is the record this
|
|
283
|
+
* runtime already wrote, and adding a field to it that says "this is what it
|
|
284
|
+
* always was" would make every pre-APRV-370 record read as though it were
|
|
285
|
+
* missing something.
|
|
286
|
+
*/
|
|
287
|
+
export interface RecordedSender {
|
|
288
|
+
channel: string;
|
|
289
|
+
id: string;
|
|
290
|
+
hashed?: true;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Every `(channel, id)` a policy maps, to the approver ids claiming it.
|
|
294
|
+
*
|
|
295
|
+
* The policy is written person to sender, so a reader sees each human's
|
|
296
|
+
* identity in one place; this is the inversion the decision path needs, and it
|
|
297
|
+
* is computed rather than stored so the two cannot disagree. A value with more
|
|
298
|
+
* than one member is the ambiguity {@link checkSenderMappings} refuses at load.
|
|
299
|
+
*
|
|
300
|
+
* Approver ids are sorted, so one policy always produces the same message.
|
|
301
|
+
*/
|
|
302
|
+
export declare function senderIndex(approvers: Record<string, {
|
|
303
|
+
channels: string[];
|
|
304
|
+
senders?: Record<string, string>;
|
|
305
|
+
}> | undefined): Map<string, string[]>;
|
|
306
|
+
/**
|
|
307
|
+
* Does this policy map any sender for `channel`?
|
|
308
|
+
*
|
|
309
|
+
* The migration switch of design §6: false is mode 1 for that channel, and the
|
|
310
|
+
* behaviour is exactly the listener identity of every build before this one.
|
|
311
|
+
*/
|
|
312
|
+
export declare function mapsSendersFor(approvers: Record<string, {
|
|
313
|
+
channels: string[];
|
|
314
|
+
senders?: Record<string, string>;
|
|
315
|
+
}> | undefined, channel: string): boolean;
|
|
316
|
+
/** Which forms this policy's mapping for `channel` uses (APRV-370). */
|
|
317
|
+
export interface SenderMappingForms {
|
|
318
|
+
/** At least one value is a decimal account id. */
|
|
319
|
+
raw: boolean;
|
|
320
|
+
/** At least one value is a {@link hashedSenderId} digest. */
|
|
321
|
+
keyed: boolean;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Which form, or forms, a policy maps `channel`'s senders in.
|
|
325
|
+
*
|
|
326
|
+
* Both flags can be true: nothing forbids a policy that names one approver
|
|
327
|
+
* raw and another keyed, and this runtime does not refuse one. What it does is
|
|
328
|
+
* refuse every tap on such a channel when the key is missing, because the half
|
|
329
|
+
* it cannot evaluate is still part of the roster it is checking for ambiguity.
|
|
330
|
+
* `approval doctor` reports the mixed state so an operator can finish the
|
|
331
|
+
* migration rather than discover it at a tap.
|
|
332
|
+
*/
|
|
333
|
+
export declare function senderMappingForms(approvers: Record<string, {
|
|
334
|
+
channels: string[];
|
|
335
|
+
senders?: Record<string, string>;
|
|
336
|
+
}> | undefined, channel: string): SenderMappingForms;
|
|
337
|
+
/**
|
|
338
|
+
* The load-time check of design §3.2 mode 4: one sender id, at most one person.
|
|
339
|
+
*
|
|
340
|
+
* Returns the message, or `null` when the policy is clear. `core/policy-load.ts`
|
|
341
|
+
* turns a message into a `sender-ambiguous` load failure, which means the
|
|
342
|
+
* policy does not load, which means every class resolves `manual` (SPEC.md
|
|
343
|
+
* §5.2, fail closed). That is the strictest answer available and the only
|
|
344
|
+
* honest one: the file says two people are one account, and there is no reading
|
|
345
|
+
* of it under which a decision by that account names a person.
|
|
346
|
+
*/
|
|
347
|
+
export declare function checkSenderMappings(approvers: Record<string, {
|
|
348
|
+
channels: string[];
|
|
349
|
+
senders?: Record<string, string>;
|
|
350
|
+
}> | undefined): string | null;
|
|
351
|
+
/**
|
|
352
|
+
* Resolve one observed sender against the policy in force (design §3.2,
|
|
353
|
+
* APRV-370).
|
|
354
|
+
*
|
|
355
|
+
* Total, pure, and the whole of the decision-time logic. It never reads a file,
|
|
356
|
+
* never reads the log, never reads the environment and never decides anything:
|
|
357
|
+
* the caller (`channels/contract.ts`) turns a `mapped` into the actor it hands
|
|
358
|
+
* the gate, and a refusal into an `audit.decision_refused` and a message to the
|
|
359
|
+
* chat.
|
|
360
|
+
*
|
|
361
|
+
* `sender` absent is mode 1 and the reason the CLI channel and every web post
|
|
362
|
+
* are untouched by this: neither supplies one, so neither can claim anybody.
|
|
363
|
+
*
|
|
364
|
+
* `key` is the operator's sender key, or `null` for none, and it DEFAULTS TO
|
|
365
|
+
* NULL. That default is the fail-closed direction and it is load-bearing: a
|
|
366
|
+
* caller that has not been taught about the key refuses every keyed mapping
|
|
367
|
+
* rather than silently comparing raw ids against digests and finding nothing,
|
|
368
|
+
* which would read exactly like a stranger tapping.
|
|
369
|
+
*/
|
|
370
|
+
export declare function resolveSender(load: PolicyLoadResult, sender: ChannelSender | undefined, key?: string | null): SenderResolution;
|
|
371
|
+
/**
|
|
372
|
+
* The sender as a record should carry it (APRV-370).
|
|
373
|
+
*
|
|
374
|
+
* `key` is the key when this channel's mapping is KEYED, and `null` when it is
|
|
375
|
+
* raw. A raw mapping records what it always recorded, which is why a keyed
|
|
376
|
+
* channel is the only thing that changes any existing record's bytes.
|
|
377
|
+
*
|
|
378
|
+
* Note which id is hashed here: the OBSERVED one, from the transport. The
|
|
379
|
+
* digest of an id the policy did not name is exactly the value a refusal wants
|
|
380
|
+
* an operator to see, because it is the line they would paste to map that
|
|
381
|
+
* account, and it discloses the account to nobody who does not already hold the
|
|
382
|
+
* key.
|
|
383
|
+
*/
|
|
384
|
+
export declare function recordedSender(sender: ChannelSender, key: string | null): RecordedSender;
|
|
385
|
+
/**
|
|
386
|
+
* The recorded form for a sender on a refusal that never reached
|
|
387
|
+
* {@link resolveSender} (APRV-370).
|
|
388
|
+
*
|
|
389
|
+
* A gesture can be refused before the mapping is consulted at all: the log
|
|
390
|
+
* could not be read, or the policy on disk is not the attested one, so its
|
|
391
|
+
* mapping is not in force. Those records still say who tried, and the question
|
|
392
|
+
* is which FORM that says it in.
|
|
393
|
+
*
|
|
394
|
+
* THE RULE: the form follows the FILE; the mapping follows the ATTESTATION. The
|
|
395
|
+
* form is a disclosure preference the operator wrote down, and honouring it
|
|
396
|
+
* from an unattested file grants nobody anything — the refusal is a refusal
|
|
397
|
+
* either way, and the record names no approver. What must never follow an
|
|
398
|
+
* unattested file is who may decide, and that is decided by
|
|
399
|
+
* {@link resolveSender} against the policy in force, which these paths never
|
|
400
|
+
* reach.
|
|
401
|
+
*
|
|
402
|
+
* A load that failed, or a channel this file maps raw, or no key: the raw id,
|
|
403
|
+
* exactly as every build since APRV-324 recorded it.
|
|
404
|
+
*/
|
|
405
|
+
export declare function recordedSenderFor(load: PolicyLoadResult, sender: ChannelSender, key: string | null): RecordedSender;
|
|
406
|
+
/**
|
|
407
|
+
* Every `(channel, id)` pair a policy declares, as a sorted, comparable list.
|
|
408
|
+
*
|
|
409
|
+
* The shape {@link sendersDiffer} compares. Sorted and flattened so that two
|
|
410
|
+
* policies differing only in key order are the same mapping, and a policy whose
|
|
411
|
+
* load failed is `null` rather than an empty mapping — "no senders" and "I
|
|
412
|
+
* could not read the senders" are different answers and only one of them is
|
|
413
|
+
* safe to act on.
|
|
414
|
+
*/
|
|
415
|
+
export declare function senderPairs(load: PolicyLoadResult): string[] | null;
|
|
416
|
+
/**
|
|
417
|
+
* Does the amendment change who may be recognized, anywhere, on any channel?
|
|
418
|
+
*
|
|
419
|
+
* The question the attestation rule turns on (§10.3). An amendment that adds,
|
|
420
|
+
* removes or repoints ANY `senders` entry is an amendment about the identity
|
|
421
|
+
* system itself, and the policy in force is the only honest place to ask who
|
|
422
|
+
* may sign for it: asking the proposed file would let whoever wrote it name
|
|
423
|
+
* themselves as the approver of their own edit.
|
|
424
|
+
*
|
|
425
|
+
* `true` when either side could not be read, which is the strict direction: an
|
|
426
|
+
* unreadable mapping is one this runtime cannot prove is unchanged.
|
|
427
|
+
*/
|
|
428
|
+
export declare function sendersDiffer(before: PolicyLoadResult, after: PolicyLoadResult): boolean;
|
|
429
|
+
/**
|
|
430
|
+
* The actor a surface records, from the sender it observed and the policy it
|
|
431
|
+
* resolved against — or the refusal that replaces it.
|
|
432
|
+
*
|
|
433
|
+
* One function, every surface (APRV-324 follow-up). Decisions, checkpoint
|
|
434
|
+
* signatures and retrospective reviews all ask the same question of the same
|
|
435
|
+
* two inputs, and three copies of the ladder would be three chances for one of
|
|
436
|
+
* them to keep the pre-mapping behaviour on the gesture that matters most.
|
|
437
|
+
* What differs between the surfaces is what they do with the answer, which is
|
|
438
|
+
* theirs; what must not differ is who the answer names.
|
|
439
|
+
*/
|
|
440
|
+
export type SenderActorResolution = {
|
|
441
|
+
ok: true;
|
|
442
|
+
actor: string;
|
|
443
|
+
/**
|
|
444
|
+
* Present only where the actor was RESOLVED from the sender, and in the
|
|
445
|
+
* form the record carries: raw, or the keyed digest (APRV-370).
|
|
446
|
+
*/
|
|
447
|
+
sender?: RecordedSender;
|
|
448
|
+
source?: SenderSource;
|
|
449
|
+
} | {
|
|
450
|
+
ok: false;
|
|
451
|
+
code: ChannelDecisionRefusalCode;
|
|
452
|
+
message: string;
|
|
453
|
+
sender: RecordedSender;
|
|
454
|
+
};
|
|
455
|
+
/**
|
|
456
|
+
* Resolve `sender` against `load`, falling back to `configured`.
|
|
457
|
+
*
|
|
458
|
+
* The `configured` actor is what the surface was launched as, and it survives
|
|
459
|
+
* exactly the two modes {@link resolveSender} calls `configured`: no sender
|
|
460
|
+
* observed, and no mapping declared for the channel that observed one.
|
|
461
|
+
*
|
|
462
|
+
* `key` defaults to `null` for the reason {@link resolveSender}'s does: a
|
|
463
|
+
* caller that does not supply one refuses a keyed mapping rather than reading
|
|
464
|
+
* past it.
|
|
465
|
+
*/
|
|
466
|
+
export declare function actorForSender(load: PolicyLoadResult, configured: string, sender: ChannelSender | undefined, key?: string | null): SenderActorResolution;
|
|
467
|
+
/**
|
|
468
|
+
* What the person who tapped is told, in one line (APRV-235's rule, applied to
|
|
469
|
+
* these codes).
|
|
470
|
+
*
|
|
471
|
+
* It names the code and says nothing about the mapping: not who is mapped, not
|
|
472
|
+
* how many are, and not the id it saw. The chat is shared, the id belongs in
|
|
473
|
+
* the log where an operator reads it deliberately, and a refusal that recited
|
|
474
|
+
* the roster would turn a mis-tap into a disclosure.
|
|
475
|
+
*/
|
|
476
|
+
export declare function senderRefusalLine(code: ChannelDecisionRefusalCode): string;
|