@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 +50 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +206 -7
- package/dist/index.js.map +6 -4
- package/dist/manifest.js +34 -3
- package/dist/manifest.js.map +4 -4
- package/dist/manifest.json +25 -1
- package/dist/operations.d.ts +87 -0
- package/package.json +4 -3
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:
|
|
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 (
|
|
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 (
|
|
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=
|
|
998
|
+
//# debugId=733FE5935B42EE2D64756E2164756E21
|
|
800
999
|
//# sourceMappingURL=index.js.map
|