@absolutejs/secrets 0.5.3 → 0.6.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 CHANGED
@@ -17,6 +17,56 @@ SB-6 substrate are `@absolutejs/sync`'s `bridgeFetch.authorization()` hook
17
17
  declarations (per-customer host functions like Stripe charge, Slack ping,
18
18
  queue push).
19
19
 
20
+ ## Agent credential operations
21
+
22
+ Agents should not call `resolve()` and should never receive a bearer token.
23
+ `createCredentialOperationBroker()` instead gives them bounded capabilities:
24
+ an exact agent and user, provider, operation scopes, URL origins, expiry, and
25
+ maximum use count. The host resolves the secret only after the grant is
26
+ atomically consumed, then passes it directly to an allowlisted provider
27
+ operation. Results and audit events contain digests and identifiers, never the
28
+ credential.
29
+
30
+ ```ts
31
+ const grants = createMemoryCredentialGrantStore();
32
+ await grants.put({
33
+ agentId: 'research-agent',
34
+ allowedOrigins: ['https://api.example.com'],
35
+ createdAt: Date.now(),
36
+ expiresAt: Date.now() + 60_000,
37
+ grantId: 'grant_123',
38
+ maximumUses: 1,
39
+ provider: 'example',
40
+ scopes: ['create-report'],
41
+ secretName: 'EXAMPLE_API_KEY',
42
+ used: 0,
43
+ userId: 'user_123',
44
+ });
45
+
46
+ const operations = createCredentialOperationBroker({
47
+ agency,
48
+ providers: [{
49
+ provider: 'example',
50
+ operations: {
51
+ 'create-report': ({ credential, destination, input, signal }) =>
52
+ fetch(new URL('/reports', destination), {
53
+ body: JSON.stringify(input),
54
+ headers: { authorization: `Bearer ${credential}` },
55
+ method: 'POST',
56
+ signal,
57
+ }).then((response) => response.json()),
58
+ },
59
+ }],
60
+ secrets: broker,
61
+ store: grants,
62
+ });
63
+ ```
64
+
65
+ Use a durable `CredentialGrantStore` in production. Passing an `agency` adds
66
+ policy, approval, single-use execution lease, and receipt enforcement.
67
+ Requestable denials can be resumed after approval with
68
+ `operations.resume(actionId)`.
69
+
20
70
  ```ts
21
71
  import { createSecretBroker, envAdapter, inMemoryAdapter, compositeAdapter } from '@absolutejs/secrets';
22
72
 
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@
24
24
  * The broker is bun/elysia-agnostic — same posture as router + meter.
25
25
  */
26
26
  import { type TracerProvider } from '@absolutejs/telemetry';
27
+ export { createCredentialOperationBroker, createMemoryCredentialGrantStore, type CredentialGrant, type CredentialGrantStore, type CredentialOperationBroker, type CredentialOperationBrokerOptions, type CredentialOperationContext, type CredentialOperationEvent, type CredentialOperationInput, type CredentialOperationResult, type CredentialProvider, type PendingCredentialOperation } from './operations';
27
28
  export type SecretValue = {
28
29
  /** The plaintext secret. Treat as poison: never log, never serialize. */
29
30
  value: string;
package/dist/index.js CHANGED
@@ -68,6 +68,193 @@ var ABS_ATTRS = {
68
68
  auditKind: "abs.audit.kind"
69
69
  };
70
70
 
71
+ // node_modules/@absolutejs/agency/dist/index.js
72
+ var normalize = (value) => {
73
+ if (Array.isArray(value))
74
+ return value.map(normalize);
75
+ if (value !== null && typeof value === "object") {
76
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalize(entry)]));
77
+ }
78
+ return value;
79
+ };
80
+ var canonicalJson = (value) => JSON.stringify(normalize(value));
81
+ var digest = async (value) => {
82
+ const encoded = new TextEncoder().encode(canonicalJson(value));
83
+ const result = await crypto.subtle.digest("SHA-256", encoded);
84
+ return [...new Uint8Array(result)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
85
+ };
86
+
87
+ // src/operations.ts
88
+ var normalizeOrigin = (destination) => new URL(destination).origin;
89
+ var validateGrant = (grant, input, now) => {
90
+ if (grant.agentId !== input.actor.agentId || grant.userId !== input.actor.userId)
91
+ throw new Error("Credential grant actor mismatch");
92
+ if (grant.expiresAt <= now)
93
+ throw new Error("Credential grant has expired");
94
+ if (!grant.scopes.includes(input.operation))
95
+ throw new Error("Credential operation is outside grant scope");
96
+ const origin = normalizeOrigin(input.destination);
97
+ if (!grant.allowedOrigins.map(normalizeOrigin).includes(origin))
98
+ throw new Error("Credential destination is outside grant scope");
99
+ if (grant.used >= grant.maximumUses)
100
+ throw new Error("Credential grant has been exhausted");
101
+ return origin;
102
+ };
103
+ var createMemoryCredentialGrantStore = () => {
104
+ const grants = new Map;
105
+ const pending = new Map;
106
+ let lock = Promise.resolve();
107
+ const exclusive = async (run) => {
108
+ const previous = lock;
109
+ let release = () => {};
110
+ lock = new Promise((resolve) => {
111
+ release = resolve;
112
+ });
113
+ await previous;
114
+ try {
115
+ return await run();
116
+ } finally {
117
+ release();
118
+ }
119
+ };
120
+ return {
121
+ consume: (grantId, now) => exclusive(() => {
122
+ const grant = grants.get(grantId);
123
+ if (grant === undefined || grant.expiresAt <= now || grant.used >= grant.maximumUses)
124
+ return;
125
+ const consumed = { ...grant, used: grant.used + 1 };
126
+ grants.set(grantId, consumed);
127
+ return { ...consumed };
128
+ }),
129
+ get: async (grantId) => {
130
+ const grant = grants.get(grantId);
131
+ return grant === undefined ? undefined : { ...grant };
132
+ },
133
+ getPending: async (actionId) => pending.get(actionId),
134
+ put: async (grant) => {
135
+ if (grant.maximumUses < 1)
136
+ throw new Error("maximumUses must be positive");
137
+ if (grant.used < 0 || grant.used > grant.maximumUses)
138
+ throw new Error("Invalid credential grant usage");
139
+ grants.set(grant.grantId, { ...grant });
140
+ },
141
+ putPending: async (operation) => {
142
+ pending.set(operation.actionId, operation);
143
+ },
144
+ revoke: async (grantId) => grants.delete(grantId)
145
+ };
146
+ };
147
+ var createCredentialOperationBroker = ({
148
+ agency,
149
+ emit,
150
+ now = Date.now,
151
+ providers,
152
+ secrets,
153
+ store
154
+ }) => {
155
+ const providerMap = new Map(providers.map((provider) => [provider.provider, provider]));
156
+ const perform = async (input, actionId, signal) => {
157
+ const preview = await store.get(input.grantId);
158
+ if (preview === undefined)
159
+ throw new Error("Unknown credential grant");
160
+ const origin = validateGrant(preview, input, now());
161
+ const provider = providerMap.get(preview.provider);
162
+ const operation = provider?.operations[input.operation];
163
+ if (provider === undefined || operation === undefined)
164
+ throw new Error("Credential provider operation is not registered");
165
+ await emit?.({
166
+ actionId,
167
+ destinationOrigin: origin,
168
+ grantId: preview.grantId,
169
+ operation: input.operation,
170
+ provider: preview.provider,
171
+ type: "credential.operation.requested"
172
+ });
173
+ const consumed = await store.consume(input.grantId, now());
174
+ if (consumed === undefined)
175
+ throw new Error("Credential grant has been exhausted");
176
+ validateGrant({ ...consumed, used: consumed.used - 1 }, input, now());
177
+ const secret = await secrets.resolve(consumed.secretName);
178
+ if (secret === null)
179
+ throw new Error("Credential is not configured");
180
+ try {
181
+ const result = await operation({
182
+ credential: secret.value,
183
+ destination: new URL(input.destination),
184
+ input: input.input,
185
+ signal
186
+ });
187
+ const resultDigest = await digest(result);
188
+ await emit?.({
189
+ actionId,
190
+ destinationOrigin: origin,
191
+ grantId: consumed.grantId,
192
+ operation: input.operation,
193
+ provider: consumed.provider,
194
+ resultDigest,
195
+ status: "succeeded",
196
+ type: "credential.operation.completed"
197
+ });
198
+ return { actionId, kind: "completed", result, resultDigest };
199
+ } catch (error) {
200
+ await emit?.({
201
+ actionId,
202
+ destinationOrigin: origin,
203
+ grantId: consumed.grantId,
204
+ operation: input.operation,
205
+ provider: consumed.provider,
206
+ status: "failed",
207
+ type: "credential.operation.completed"
208
+ });
209
+ throw error;
210
+ }
211
+ };
212
+ const request = async (input, signal) => {
213
+ const grant = await store.get(input.grantId);
214
+ if (grant === undefined)
215
+ throw new Error("Unknown credential grant");
216
+ const origin = validateGrant(grant, input, now());
217
+ if (agency === undefined)
218
+ return perform(input, undefined, signal);
219
+ const { action, decision } = await agency.request({
220
+ action: `credential.${grant.provider}.${input.operation}`,
221
+ actor: input.actor,
222
+ context: { destinationOrigin: origin, grantId: grant.grantId },
223
+ effects: ["external-network"],
224
+ expiresAt: grant.expiresAt,
225
+ idempotencyKey: input.idempotencyKey,
226
+ input: { destination: input.destination, input: input.input },
227
+ resource: { id: grant.grantId, type: "credential-grant" }
228
+ });
229
+ if (decision.kind !== "allow") {
230
+ await store.putPending({ actionId: action.actionId, input });
231
+ return { actionId: action.actionId, decision, kind: "pending" };
232
+ }
233
+ const lease = await agency.issueLease(action.actionId);
234
+ const execution = await agency.execute({
235
+ executor: `credential-provider:${grant.provider}`,
236
+ leaseId: lease.leaseId,
237
+ run: () => perform(input, action.actionId, signal)
238
+ });
239
+ return execution.result;
240
+ };
241
+ const resume = async (actionId, signal) => {
242
+ if (agency === undefined)
243
+ throw new Error("Agency is not configured");
244
+ const pending = await store.getPending(actionId);
245
+ if (pending === undefined)
246
+ throw new Error("Unknown pending credential operation");
247
+ const lease = await agency.issueLease(actionId);
248
+ const execution = await agency.execute({
249
+ executor: "credential-provider",
250
+ leaseId: lease.leaseId,
251
+ run: () => perform(pending.input, actionId, signal)
252
+ });
253
+ return execution.result;
254
+ };
255
+ return { request, resume, revoke: store.revoke };
256
+ };
257
+
71
258
  // src/index.ts
72
259
  class BrokerDrainedError extends Error {
73
260
  constructor() {
@@ -802,9 +989,11 @@ export {
802
989
  envAdapter,
803
990
  encryptedFileAdapter,
804
991
  createSecretBroker,
992
+ createMemoryCredentialGrantStore,
993
+ createCredentialOperationBroker,
805
994
  compositeAdapter,
806
995
  BrokerDrainedError
807
996
  };
808
997
 
809
- //# debugId=51F9793EA6C3ED2F64756E2164756E21
998
+ //# debugId=733FE5935B42EE2D64756E2164756E21
810
999
  //# sourceMappingURL=index.js.map