@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 +50 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +190 -1
- 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() {
|
|
@@ -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=
|
|
998
|
+
//# debugId=733FE5935B42EE2D64756E2164756E21
|
|
810
999
|
//# sourceMappingURL=index.js.map
|