@absolutejs/secrets 0.5.2 → 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() {
@@ -352,7 +539,17 @@ var encryptedFileAdapter = (options) => {
352
539
  const rotate = options.rotate ?? (() => randomBase36(32));
353
540
  let cache;
354
541
  let derivedKey;
542
+ let mutationQueue = Promise.resolve();
355
543
  let salt;
544
+ const mutate = (operation) => {
545
+ const result = mutationQueue.then(operation, operation);
546
+ mutationQueue = result.then(() => {
547
+ return;
548
+ }, () => {
549
+ return;
550
+ });
551
+ return result;
552
+ };
356
553
  const ensureKey = async () => {
357
554
  if (derivedKey !== undefined)
358
555
  return derivedKey;
@@ -440,24 +637,24 @@ var encryptedFileAdapter = (options) => {
440
637
  const data = await load();
441
638
  return Array.from(data.keys());
442
639
  },
443
- put: async (name, value) => {
640
+ put: (name, value) => mutate(async () => {
444
641
  const data = await load();
445
642
  data.set(name, value);
446
643
  await save();
447
- },
448
- remove: async (name) => {
644
+ }),
645
+ remove: (name) => mutate(async () => {
449
646
  const data = await load();
450
647
  data.delete(name);
451
648
  await save();
452
- },
453
- rotate: async (name) => {
649
+ }),
650
+ rotate: (name) => mutate(async () => {
454
651
  const data = await load();
455
652
  const previous = data.get(name) ?? null;
456
653
  const next = rotate(name, previous);
457
654
  data.set(name, next);
458
655
  await save();
459
656
  return next;
460
- }
657
+ })
461
658
  };
462
659
  };
463
660
  var rotateMasterKey = async (options) => {
@@ -792,9 +989,11 @@ export {
792
989
  envAdapter,
793
990
  encryptedFileAdapter,
794
991
  createSecretBroker,
992
+ createMemoryCredentialGrantStore,
993
+ createCredentialOperationBroker,
795
994
  compositeAdapter,
796
995
  BrokerDrainedError
797
996
  };
798
997
 
799
- //# debugId=99DB80DCBB4A8D8764756E2164756E21
998
+ //# debugId=733FE5935B42EE2D64756E2164756E21
800
999
  //# sourceMappingURL=index.js.map