@atlasauth/pca-ciba 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +63 -2
- package/dist/index.d.ts +147 -0
- package/dist/index.js +181 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atlas Authorization
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @atlasauth/pca-ciba
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A CIBA (OpenID Connect Client-Initiated Backchannel Authentication) on-ramp for Proof-Carrying Authority (PCA) step-up. It turns a risk-gated step-up into a standard asynchronous human-approval flow, interrupting a person only for the actions that actually need it.
|
|
4
|
+
|
|
5
|
+
In CIBA a backend opens an `auth_req_id`, the human is notified out of band, and the decision comes back by poll or ping/push. The `binding_message` carries the human-readable approval context, so the thing approved on the phone is the thing the agent is about to do ("Approve stripe.refund $480 on charge:ch_1 - because you asked support-bot to 'reconcile October refunds'").
|
|
6
|
+
|
|
7
|
+
This is a bridge, not a replacement. PCA's cryptographic step-up (a FROST threshold cosign, in `@atlasauth/pca`) stays the enforcement; CIBA is the notification and decision envelope around it.
|
|
8
|
+
|
|
9
|
+
Gating is risk-adaptive: only a tier-3 step-up (human required, typically irreversible) is routed through CIBA. Tier-2 (guardian auto-cosign) and tier-1 (`auto`) never interrupt a person.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm i @atlasauth/pca-ciba @atlasauth/pca
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { agent, generateKeyPair, reviewAction } from '@atlasauth/pca';
|
|
21
|
+
import { requiresCiba, createCibaBroker, cibaDecisionToCosign } from '@atlasauth/pca-ciba';
|
|
22
|
+
|
|
23
|
+
const a = agent({
|
|
24
|
+
principal: generateKeyPair(),
|
|
25
|
+
goal: 'reconcile October refunds',
|
|
26
|
+
permissions: { stripe: ['payout'] },
|
|
27
|
+
limits: { payout: '$100' },
|
|
28
|
+
aud: 'rs_demo',
|
|
29
|
+
riskPolicy: { theta1: 0.3, theta2: 0.6 },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const review = reviewAction(a, 'stripe.payout', 'acct:1', { amount: 80 }, { goal: 'reconcile October refunds' });
|
|
33
|
+
|
|
34
|
+
if (review.kind === 'step_up' && requiresCiba(review)) {
|
|
35
|
+
const broker = createCibaBroker({ onComplete: (session) => console.log(session.status) }); // omit onComplete for POLL
|
|
36
|
+
const ciba = broker.start(review.request, { actsFor: 'support-bot', loginHint: 'owner@acme' });
|
|
37
|
+
// ciba.auth_req_id, ciba.binding_message, ciba.requested_expiry, ...
|
|
38
|
+
|
|
39
|
+
broker.poll(ciba.auth_req_id).status; // 'pending' | 'approved' | 'denied' | 'expired'
|
|
40
|
+
|
|
41
|
+
// The human decides out of band:
|
|
42
|
+
const session = broker.resolve(ciba.auth_req_id, 'approve', 'owner@acme');
|
|
43
|
+
|
|
44
|
+
// Fold the decision back into the trust budget:
|
|
45
|
+
const { budget, cosigned } = cibaDecisionToCosign(
|
|
46
|
+
session.status === 'approved' ? 'approve' : 'deny',
|
|
47
|
+
a.budget,
|
|
48
|
+
a.policy.riskPolicy,
|
|
49
|
+
Date.now(),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`toCibaAuthRequest(stepUp, opts?)` is the pure mapping from a step-up to a CIBA request, if you want to drive your own broker or a real OpenID provider.
|
|
55
|
+
|
|
56
|
+
## Status
|
|
57
|
+
|
|
58
|
+
The broker is in-memory and models the CIBA lifecycle without any network; a production deployment would swap in a real OpenID provider. The package computes approval envelopes and the derived trust budget and authorises nothing on its own: `cibaDecisionToCosign` is only the budget-level effect. The cryptographic cosign (FROST) is produced and verified separately, and the resource server's verifier remains the authority. Cryptography in PCA is unaudited.
|
|
59
|
+
|
|
60
|
+
Source and issues: https://github.com/Atlas-Authorization/pca
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT - see LICENSE
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atlasauth/pca-ciba — a CIBA on-ramp for PCA's FROST step-up.
|
|
3
|
+
*
|
|
4
|
+
* CIBA (OpenID Connect Client-Initiated Backchannel Authentication, OpenID CIBA Core) is the
|
|
5
|
+
* convergent human-in-the-loop (HITL) primitive every modern IdP ships — Auth0 ("CIBA / push
|
|
6
|
+
* authorization"), Descope, Stytch — for *asynchronous* out-of-band approval: a backend starts an
|
|
7
|
+
* `auth_req_id`, the human is notified on a separate device, and the backend learns the decision by
|
|
8
|
+
* polling (POLL) or via a callback (PING/PUSH). Its `binding_message` field carries the short
|
|
9
|
+
* human-readable context the person sees while approving, so the thing they approve on their phone is
|
|
10
|
+
* provably the thing the agent is about to do.
|
|
11
|
+
*
|
|
12
|
+
* This package is a BRIDGE, not a replacement. PCA's cryptographic step-up (FROST threshold cosign —
|
|
13
|
+
* see `@atlasauth/pca` `threshold`/`frost`) stays the *enforcement*: a tier-3 action proceeds only once
|
|
14
|
+
* a real threshold signature recharges the trust budget. CIBA is only the human-notification / decision
|
|
15
|
+
* *envelope* around that cosign — it standardises how the human is asked and how the answer comes back.
|
|
16
|
+
* PCA's FROST step-up is cryptographically stronger than a bare CIBA approval (an OP's yes/no is a
|
|
17
|
+
* trusted single party; a FROST cosign is an unforgeable t-of-n signature); this just gives it the
|
|
18
|
+
* industry-standard async-approval + binding-message on-ramp.
|
|
19
|
+
*
|
|
20
|
+
* Risk-adaptive gating is the point. If every action prompts a human, humans blanket-approve (Anthropic
|
|
21
|
+
* observed 93% blanket-approval under always-prompt). So only a tier-3 step-up — a human-required,
|
|
22
|
+
* typically irreversible/high-risk action — is routed through CIBA. Tier-2 (guardian auto-cosign) and
|
|
23
|
+
* tier-1 (`auto`) never interrupt a person. `requiresCiba()` encodes exactly that gate.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is pure or in-memory and authorises nothing on its own: the resource server's verifier
|
|
26
|
+
* (`requirePCA` / the adjudicator) and the FROST cosigners remain the run-time authority.
|
|
27
|
+
*/
|
|
28
|
+
import { type StepUpRequest, type Review, type TrustBudget, type RiskPolicy } from '@atlasauth/pca';
|
|
29
|
+
/** A CIBA backchannel authentication request derived from a PCA step-up. */
|
|
30
|
+
export interface CibaAuthRequest {
|
|
31
|
+
/**
|
|
32
|
+
* The CIBA `auth_req_id` — the opaque handle the backend polls / correlates on. Derived
|
|
33
|
+
* deterministically from the step-up id, so the same step-up always maps to the same request.
|
|
34
|
+
*/
|
|
35
|
+
auth_req_id: string;
|
|
36
|
+
/**
|
|
37
|
+
* The human-readable approval context shown to the person (OpenID CIBA `binding_message`): what is
|
|
38
|
+
* being approved and why, traced to the goal the agent was given.
|
|
39
|
+
*/
|
|
40
|
+
binding_message: string;
|
|
41
|
+
/** CIBA `scope` (space-delimited), carrying `openid` + a PCA step-up scope for this verb. */
|
|
42
|
+
scope: string;
|
|
43
|
+
/** CIBA `requested_expiry` in SECONDS — how long the human has to decide. */
|
|
44
|
+
requested_expiry: number;
|
|
45
|
+
/** CIBA `interval` in SECONDS — minimum poll spacing for POLL mode. */
|
|
46
|
+
interval: number;
|
|
47
|
+
/** CIBA `login_hint` — who to notify, if the caller supplied one. */
|
|
48
|
+
login_hint?: string;
|
|
49
|
+
/** The PCA step-up this request fulfils (back-link for correlation). */
|
|
50
|
+
stepUpId: string;
|
|
51
|
+
/** The PCA risk tier that produced the step-up (2 guardian / 3 human). */
|
|
52
|
+
tier: 2 | 3;
|
|
53
|
+
}
|
|
54
|
+
/** Options shared by `toCibaAuthRequest` and the broker's `start`. */
|
|
55
|
+
export interface CibaRequestOptions {
|
|
56
|
+
/** Override the `requested_expiry` (seconds). Default 300. */
|
|
57
|
+
expiresInSec?: number;
|
|
58
|
+
/** CIBA `login_hint` for the OP: the identifier of the human to notify. */
|
|
59
|
+
loginHint?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The agent's human-facing name used in the binding message ("because you asked <actsFor> to …"),
|
|
62
|
+
* e.g. `"support-bot"`. Default `"your agent"`.
|
|
63
|
+
*/
|
|
64
|
+
actsFor?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Map a PCA {@link StepUpRequest} to a {@link CibaAuthRequest}. Pure: derives a stable `auth_req_id`
|
|
68
|
+
* from the step-up id, renders the `binding_message`, and carries the scope / expiry / poll interval.
|
|
69
|
+
* Does not start a flow or notify anyone — that's the broker.
|
|
70
|
+
*/
|
|
71
|
+
export declare function toCibaAuthRequest(stepUp: StepUpRequest, opts?: CibaRequestOptions): CibaAuthRequest;
|
|
72
|
+
/** The settled outcome of a CIBA request, with `pending` while the human hasn't acted and `expired`
|
|
73
|
+
* once `requested_expiry` elapses. */
|
|
74
|
+
export type CibaStatus = 'pending' | 'approved' | 'denied' | 'expired';
|
|
75
|
+
/** A broker session: the request plus the (lazily-evaluated) decision state. */
|
|
76
|
+
export interface CibaSession {
|
|
77
|
+
auth_req_id: string;
|
|
78
|
+
request: CibaAuthRequest;
|
|
79
|
+
/** Effective status computed at read time (never a stale stored flag). */
|
|
80
|
+
status: CibaStatus;
|
|
81
|
+
/** ms epoch the session was started. */
|
|
82
|
+
createdAt: number;
|
|
83
|
+
/** ms epoch the request expires (`createdAt + requested_expiry·1000`). */
|
|
84
|
+
expiresAt: number;
|
|
85
|
+
/** Who acted, once resolved. */
|
|
86
|
+
decidedBy?: string;
|
|
87
|
+
/** ms epoch the human acted, once resolved. */
|
|
88
|
+
decidedAt?: number;
|
|
89
|
+
}
|
|
90
|
+
/** Options for {@link createCibaBroker}. */
|
|
91
|
+
export interface CibaBrokerOptions {
|
|
92
|
+
/** Clock (ms). Default `Date.now`. Inject for deterministic tests / expiry. */
|
|
93
|
+
now?: () => number;
|
|
94
|
+
/**
|
|
95
|
+
* PING/PUSH mode: called once, synchronously, the moment a session is resolved (approved or denied).
|
|
96
|
+
* Omit it to use pure POLL mode (the caller drives `poll`).
|
|
97
|
+
*/
|
|
98
|
+
onComplete?: (session: CibaSession) => void;
|
|
99
|
+
}
|
|
100
|
+
/** The in-memory CIBA broker returned by {@link createCibaBroker}. */
|
|
101
|
+
export interface CibaBroker {
|
|
102
|
+
/** Begin a backchannel flow for a step-up; returns the CIBA auth request (status starts `pending`). */
|
|
103
|
+
start(stepUp: StepUpRequest, opts?: CibaRequestOptions): CibaAuthRequest;
|
|
104
|
+
/** POLL: the current effective status (`authorization_pending` semantics while `pending`). */
|
|
105
|
+
poll(auth_req_id: string): {
|
|
106
|
+
status: CibaStatus;
|
|
107
|
+
};
|
|
108
|
+
/** The human acts out-of-band: approve or deny. Fires `onComplete` (PING/PUSH) if configured. */
|
|
109
|
+
resolve(auth_req_id: string, decision: 'approve' | 'deny', by: string): CibaSession;
|
|
110
|
+
/** Read a full session snapshot (status computed at read time), or `undefined` if unknown. */
|
|
111
|
+
get(auth_req_id: string): CibaSession | undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* An in-memory CIBA broker. Models the backchannel lifecycle without any network: `start` opens a
|
|
115
|
+
* session, `poll` reports `pending`/`approved`/`denied`/`expired`, and `resolve` records the human's
|
|
116
|
+
* decision. Supports both delivery modes — POLL (caller drives `poll`) and PING/PUSH (an `onComplete`
|
|
117
|
+
* callback fires on resolution). A production deployment swaps this for the real OP + FROST cosigners;
|
|
118
|
+
* the shape stays the same.
|
|
119
|
+
*/
|
|
120
|
+
export declare function createCibaBroker(opts?: CibaBrokerOptions): CibaBroker;
|
|
121
|
+
/**
|
|
122
|
+
* The gate: does this review need a CIBA human-approval round-trip? Only a tier-3 step-up (human
|
|
123
|
+
* required — typically irreversible / high blast-radius) does. A tier-2 step-up is handled by the
|
|
124
|
+
* guardian's auto-cosign, and an `auto`/`deny` review never notifies a human. This is the
|
|
125
|
+
* risk-adaptive interrupt that avoids blanket-approval fatigue.
|
|
126
|
+
*/
|
|
127
|
+
export declare function requiresCiba(review: Review): boolean;
|
|
128
|
+
/** The budget effect of a settled CIBA decision. */
|
|
129
|
+
export interface CibaCosignResult {
|
|
130
|
+
/** The trust budget after applying the decision (recharged on approve, unchanged on deny). */
|
|
131
|
+
budget: TrustBudget;
|
|
132
|
+
/** Whether a (budget-level) cosign was applied. */
|
|
133
|
+
cosigned: boolean;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Fold a settled CIBA decision back into PCA's trust budget. On `approve`, recharge via `applyCosign`
|
|
137
|
+
* (partial ρ per cosign, or full with `{ full: true }`); on `deny`, the budget is unchanged.
|
|
138
|
+
*
|
|
139
|
+
* NOTE: this is the BUDGET-level effect only. The actual cryptographic cosign that authorises a tier-3
|
|
140
|
+
* action is a FROST threshold signature (`@atlasauth/pca` `threshold`/`frost`), produced out of band and
|
|
141
|
+
* verified by the resource server — CIBA carries the human *decision*, not the signature. A real
|
|
142
|
+
* integration gates `applyCosign` on a verified FROST cosign; here we model the budget recharge the
|
|
143
|
+
* approval authorises.
|
|
144
|
+
*/
|
|
145
|
+
export declare function cibaDecisionToCosign(decision: 'approve' | 'deny', budget: TrustBudget, policy: RiskPolicy, now: number, opts?: {
|
|
146
|
+
full?: boolean;
|
|
147
|
+
}): CibaCosignResult;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @atlasauth/pca-ciba — a CIBA on-ramp for PCA's FROST step-up.
|
|
4
|
+
*
|
|
5
|
+
* CIBA (OpenID Connect Client-Initiated Backchannel Authentication, OpenID CIBA Core) is the
|
|
6
|
+
* convergent human-in-the-loop (HITL) primitive every modern IdP ships — Auth0 ("CIBA / push
|
|
7
|
+
* authorization"), Descope, Stytch — for *asynchronous* out-of-band approval: a backend starts an
|
|
8
|
+
* `auth_req_id`, the human is notified on a separate device, and the backend learns the decision by
|
|
9
|
+
* polling (POLL) or via a callback (PING/PUSH). Its `binding_message` field carries the short
|
|
10
|
+
* human-readable context the person sees while approving, so the thing they approve on their phone is
|
|
11
|
+
* provably the thing the agent is about to do.
|
|
12
|
+
*
|
|
13
|
+
* This package is a BRIDGE, not a replacement. PCA's cryptographic step-up (FROST threshold cosign —
|
|
14
|
+
* see `@atlasauth/pca` `threshold`/`frost`) stays the *enforcement*: a tier-3 action proceeds only once
|
|
15
|
+
* a real threshold signature recharges the trust budget. CIBA is only the human-notification / decision
|
|
16
|
+
* *envelope* around that cosign — it standardises how the human is asked and how the answer comes back.
|
|
17
|
+
* PCA's FROST step-up is cryptographically stronger than a bare CIBA approval (an OP's yes/no is a
|
|
18
|
+
* trusted single party; a FROST cosign is an unforgeable t-of-n signature); this just gives it the
|
|
19
|
+
* industry-standard async-approval + binding-message on-ramp.
|
|
20
|
+
*
|
|
21
|
+
* Risk-adaptive gating is the point. If every action prompts a human, humans blanket-approve (Anthropic
|
|
22
|
+
* observed 93% blanket-approval under always-prompt). So only a tier-3 step-up — a human-required,
|
|
23
|
+
* typically irreversible/high-risk action — is routed through CIBA. Tier-2 (guardian auto-cosign) and
|
|
24
|
+
* tier-1 (`auto`) never interrupt a person. `requiresCiba()` encodes exactly that gate.
|
|
25
|
+
*
|
|
26
|
+
* Everything here is pure or in-memory and authorises nothing on its own: the resource server's verifier
|
|
27
|
+
* (`requirePCA` / the adjudicator) and the FROST cosigners remain the run-time authority.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.toCibaAuthRequest = toCibaAuthRequest;
|
|
31
|
+
exports.createCibaBroker = createCibaBroker;
|
|
32
|
+
exports.requiresCiba = requiresCiba;
|
|
33
|
+
exports.cibaDecisionToCosign = cibaDecisionToCosign;
|
|
34
|
+
const pca_1 = require("@atlasauth/pca");
|
|
35
|
+
const DEFAULT_EXPIRY_SEC = 300;
|
|
36
|
+
const DEFAULT_INTERVAL_SEC = 5;
|
|
37
|
+
/** A short "$480" / "480 EUR" fragment from a step-up's params, or "" when there's no amount. */
|
|
38
|
+
function amountFragment(params) {
|
|
39
|
+
if (!params)
|
|
40
|
+
return '';
|
|
41
|
+
const amt = params['amount'];
|
|
42
|
+
if (typeof amt !== 'number' || !Number.isFinite(amt))
|
|
43
|
+
return '';
|
|
44
|
+
const cur = params['currency'];
|
|
45
|
+
if (typeof cur === 'string' && cur.length > 0 && cur.toLowerCase() !== 'usd') {
|
|
46
|
+
return ` ${amt} ${cur.toUpperCase()}`;
|
|
47
|
+
}
|
|
48
|
+
return ` $${amt}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the CIBA `binding_message`: the one-line human-readable approval context. Combines the
|
|
52
|
+
* verb + optional amount + resource with the goal-lineage ("because you asked <agent> to '<goal>'").
|
|
53
|
+
* Falls back to the salted goal commitment when the plaintext goal isn't carried on the step-up, so
|
|
54
|
+
* lineage is always present.
|
|
55
|
+
*/
|
|
56
|
+
function bindingMessage(stepUp, actsFor) {
|
|
57
|
+
const head = `Approve ${stepUp.verb}${amountFragment(stepUp.params)} on ${stepUp.resource}`;
|
|
58
|
+
const goalText = stepUp.goal !== undefined && stepUp.goal.length > 0
|
|
59
|
+
? stepUp.goal
|
|
60
|
+
: stepUp.goalCommit.length > 0
|
|
61
|
+
? `goal ${stepUp.goalCommit.slice(0, 10)}…`
|
|
62
|
+
: undefined;
|
|
63
|
+
return goalText === undefined ? head : `${head} — because you asked ${actsFor} to '${goalText}'`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Map a PCA {@link StepUpRequest} to a {@link CibaAuthRequest}. Pure: derives a stable `auth_req_id`
|
|
67
|
+
* from the step-up id, renders the `binding_message`, and carries the scope / expiry / poll interval.
|
|
68
|
+
* Does not start a flow or notify anyone — that's the broker.
|
|
69
|
+
*/
|
|
70
|
+
function toCibaAuthRequest(stepUp, opts = {}) {
|
|
71
|
+
const actsFor = opts.actsFor !== undefined && opts.actsFor.length > 0 ? opts.actsFor : 'your agent';
|
|
72
|
+
const auth_req_id = (0, pca_1.hashCanonical)({ d: 'atlas-pca/ciba/auth_req_id/v1', stepUp: stepUp.id });
|
|
73
|
+
return {
|
|
74
|
+
auth_req_id,
|
|
75
|
+
binding_message: bindingMessage(stepUp, actsFor),
|
|
76
|
+
scope: `openid urn:atlas:pca:stepup:${stepUp.verb}`,
|
|
77
|
+
requested_expiry: opts.expiresInSec !== undefined ? opts.expiresInSec : DEFAULT_EXPIRY_SEC,
|
|
78
|
+
interval: DEFAULT_INTERVAL_SEC,
|
|
79
|
+
...(opts.loginHint !== undefined ? { login_hint: opts.loginHint } : {}),
|
|
80
|
+
stepUpId: stepUp.id,
|
|
81
|
+
tier: stepUp.tier,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Status is computed, never stored — a decision wins, else expiry, else pending (read-time truth). */
|
|
85
|
+
function effectiveStatus(s, now) {
|
|
86
|
+
if (s.decision === 'approve')
|
|
87
|
+
return 'approved';
|
|
88
|
+
if (s.decision === 'deny')
|
|
89
|
+
return 'denied';
|
|
90
|
+
if (now >= s.expiresAt)
|
|
91
|
+
return 'expired';
|
|
92
|
+
return 'pending';
|
|
93
|
+
}
|
|
94
|
+
function snapshot(s, now) {
|
|
95
|
+
return {
|
|
96
|
+
auth_req_id: s.request.auth_req_id,
|
|
97
|
+
request: s.request,
|
|
98
|
+
status: effectiveStatus(s, now),
|
|
99
|
+
createdAt: s.createdAt,
|
|
100
|
+
expiresAt: s.expiresAt,
|
|
101
|
+
...(s.decidedBy !== undefined ? { decidedBy: s.decidedBy } : {}),
|
|
102
|
+
...(s.decidedAt !== undefined ? { decidedAt: s.decidedAt } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* An in-memory CIBA broker. Models the backchannel lifecycle without any network: `start` opens a
|
|
107
|
+
* session, `poll` reports `pending`/`approved`/`denied`/`expired`, and `resolve` records the human's
|
|
108
|
+
* decision. Supports both delivery modes — POLL (caller drives `poll`) and PING/PUSH (an `onComplete`
|
|
109
|
+
* callback fires on resolution). A production deployment swaps this for the real OP + FROST cosigners;
|
|
110
|
+
* the shape stays the same.
|
|
111
|
+
*/
|
|
112
|
+
function createCibaBroker(opts = {}) {
|
|
113
|
+
const clock = opts.now ?? Date.now;
|
|
114
|
+
const sessions = new Map();
|
|
115
|
+
return {
|
|
116
|
+
start(stepUp, reqOpts) {
|
|
117
|
+
const request = toCibaAuthRequest(stepUp, reqOpts);
|
|
118
|
+
const createdAt = clock();
|
|
119
|
+
const stored = {
|
|
120
|
+
request,
|
|
121
|
+
createdAt,
|
|
122
|
+
expiresAt: createdAt + request.requested_expiry * 1000,
|
|
123
|
+
};
|
|
124
|
+
sessions.set(request.auth_req_id, stored);
|
|
125
|
+
return request;
|
|
126
|
+
},
|
|
127
|
+
poll(auth_req_id) {
|
|
128
|
+
const s = sessions.get(auth_req_id);
|
|
129
|
+
if (!s)
|
|
130
|
+
throw new Error(`ciba: unknown auth_req_id ${auth_req_id}`);
|
|
131
|
+
return { status: effectiveStatus(s, clock()) };
|
|
132
|
+
},
|
|
133
|
+
resolve(auth_req_id, decision, by) {
|
|
134
|
+
const s = sessions.get(auth_req_id);
|
|
135
|
+
if (!s)
|
|
136
|
+
throw new Error(`ciba: unknown auth_req_id ${auth_req_id}`);
|
|
137
|
+
const now = clock();
|
|
138
|
+
const current = effectiveStatus(s, now);
|
|
139
|
+
if (current !== 'pending') {
|
|
140
|
+
throw new Error(`ciba: cannot resolve ${auth_req_id} — already ${current}`);
|
|
141
|
+
}
|
|
142
|
+
s.decision = decision;
|
|
143
|
+
s.decidedBy = by;
|
|
144
|
+
s.decidedAt = now;
|
|
145
|
+
const snap = snapshot(s, now);
|
|
146
|
+
if (opts.onComplete)
|
|
147
|
+
opts.onComplete(snap);
|
|
148
|
+
return snap;
|
|
149
|
+
},
|
|
150
|
+
get(auth_req_id) {
|
|
151
|
+
const s = sessions.get(auth_req_id);
|
|
152
|
+
return s ? snapshot(s, clock()) : undefined;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// ---- risk-adaptive gating + the hand-off back to the cryptographic cosign -------------------------
|
|
157
|
+
/**
|
|
158
|
+
* The gate: does this review need a CIBA human-approval round-trip? Only a tier-3 step-up (human
|
|
159
|
+
* required — typically irreversible / high blast-radius) does. A tier-2 step-up is handled by the
|
|
160
|
+
* guardian's auto-cosign, and an `auto`/`deny` review never notifies a human. This is the
|
|
161
|
+
* risk-adaptive interrupt that avoids blanket-approval fatigue.
|
|
162
|
+
*/
|
|
163
|
+
function requiresCiba(review) {
|
|
164
|
+
return review.kind === 'step_up' && review.request.tier === 3;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Fold a settled CIBA decision back into PCA's trust budget. On `approve`, recharge via `applyCosign`
|
|
168
|
+
* (partial ρ per cosign, or full with `{ full: true }`); on `deny`, the budget is unchanged.
|
|
169
|
+
*
|
|
170
|
+
* NOTE: this is the BUDGET-level effect only. The actual cryptographic cosign that authorises a tier-3
|
|
171
|
+
* action is a FROST threshold signature (`@atlasauth/pca` `threshold`/`frost`), produced out of band and
|
|
172
|
+
* verified by the resource server — CIBA carries the human *decision*, not the signature. A real
|
|
173
|
+
* integration gates `applyCosign` on a verified FROST cosign; here we model the budget recharge the
|
|
174
|
+
* approval authorises.
|
|
175
|
+
*/
|
|
176
|
+
function cibaDecisionToCosign(decision, budget, policy, now, opts = {}) {
|
|
177
|
+
if (decision === 'approve')
|
|
178
|
+
return { budget: (0, pca_1.applyCosign)(budget, policy, now, opts), cosigned: true };
|
|
179
|
+
return { budget, cosigned: false };
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlasauth/pca-ciba",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CIBA (Client-Initiated Backchannel Authentication) bridge for PCA FROST step-up: turn a step-up request into a backchannel human-approval flow with risk-adaptive gating.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ciba",
|
|
7
|
+
"backchannel",
|
|
8
|
+
"human approval",
|
|
9
|
+
"step-up",
|
|
10
|
+
"agent authorization",
|
|
11
|
+
"proof-carrying authority",
|
|
12
|
+
"pca",
|
|
13
|
+
"ai agents"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://github.com/Atlas-Authorization/pca#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/Atlas-Authorization/pca/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Atlas-Authorization/pca.git",
|
|
23
|
+
"directory": "packages/pca-ciba"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@atlasauth/pca": "0.2.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.6.3",
|
|
39
|
+
"vitest": "^2.1.4"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc -p tsconfig.build.json",
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
+
"test": "vitest run"
|
|
56
|
+
}
|
|
6
57
|
}
|