@atlasauth/pca-ciba 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/dist/index.d.ts +147 -0
- package/dist/index.js +181 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -3
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @atlasauth/pca-ciba
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A CIBA (OpenID Connect Client-Initiated Backchannel Authentication) on-ramp for PCA's FROST step-up: turn a risk-gated step-up into a standard async human-approval flow, interrupting a person only for the actions that actually need it.
|
|
4
|
+
|
|
5
|
+
CIBA is the convergent human-in-the-loop primitive every modern IdP ships (Auth0, Descope, Stytch): a backend opens an `auth_req_id`, the human is notified out of band, and the decision comes back by POLL or PING/PUSH. Its `binding_message` carries the human-readable approval context — so the thing approved on the phone is provably the thing the agent is about to do (OpenID CIBA Core).
|
|
6
|
+
|
|
7
|
+
This is a **bridge, not a replacement**. PCA's cryptographic step-up (FROST threshold cosign, in `@atlasauth/pca`) stays the enforcement; CIBA is the human-notification / decision envelope around it. A bare OP yes/no is a trusted single party — a FROST cosign is an unforgeable t-of-n signature — so PCA's step-up is cryptographically stronger; this just gives it the industry-standard async-approval + binding-message on-ramp.
|
|
8
|
+
|
|
9
|
+
**Risk-adaptive gating** is the point: if every action prompts, humans blanket-approve (Anthropic observed 93% blanket-approval under always-prompt). 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
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Depends on the core `@atlasauth/pca` (installed transitively).
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { reviewAction } from '@atlasauth/pca';
|
|
23
|
+
import { requiresCiba, createCibaBroker, cibaDecisionToCosign } from '@atlasauth/pca-ciba';
|
|
24
|
+
|
|
25
|
+
const review = reviewAction(agent, 'stripe.refund', 'charge:ch_1', { amount: 480 }, { goal: 'reconcile October refunds' });
|
|
26
|
+
|
|
27
|
+
if (requiresCiba(review) && review.kind === 'step_up') {
|
|
28
|
+
const broker = createCibaBroker({ onComplete: (s) => notify(s) }); // PING/PUSH; omit for POLL
|
|
29
|
+
const ciba = broker.start(review.request, { actsFor: 'support-bot', loginHint: 'owner@acme' });
|
|
30
|
+
// ciba.binding_message:
|
|
31
|
+
// "Approve stripe.refund $480 on charge:ch_1 — because you asked support-bot to 'reconcile October refunds'"
|
|
32
|
+
|
|
33
|
+
// POLL: broker.poll(ciba.auth_req_id).status → 'pending' | 'approved' | 'denied' | 'expired'
|
|
34
|
+
// The human acts out of band:
|
|
35
|
+
const session = broker.resolve(ciba.auth_req_id, 'approve', 'owner@acme');
|
|
36
|
+
|
|
37
|
+
// Fold the decision back into the trust budget (the real cryptographic cosign is FROST, separate):
|
|
38
|
+
const { budget } = cibaDecisionToCosign(session.status === 'approved' ? 'approve' : 'deny', agent.budget, agent.policy.riskPolicy, Date.now());
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`toCibaAuthRequest(stepUp, opts?)` is the pure mapping (step-up → CIBA request) if you want to drive your own broker or a real OP.
|
|
43
|
+
|
|
44
|
+
This computes approval envelopes and the derived trust budget; it authorises nothing on its own. The resource server's verifier and the FROST cosigners remain the authority at run time.
|
|
45
|
+
|
|
46
|
+
Part of Proof-Carrying Authority — see `@atlasauth/pca`.
|
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,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlasauth/pca-ciba",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.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
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@atlasauth/pca": "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.6.3",
|
|
19
|
+
"vitest": "^2.1.4"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
}
|
|
6
33
|
}
|