@keystrokehq/skills 0.0.2 → 0.0.3
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/keystroke-agent-authoring/SKILL.md +4 -4
- package/keystroke-agent-authoring/references/messaging-gateways.md +7 -7
- package/keystroke-agent-authoring/references/patterns.md +5 -5
- package/keystroke-agent-authoring/references/prebuilt-integrations.md +78 -78
- package/keystroke-agent-authoring/references/sandbox-and-mcp.md +2 -2
- package/keystroke-agent-authoring/references/source-map.md +1 -1
- package/keystroke-agent-authoring/references/testing.md +4 -4
- package/keystroke-credential-binding/SKILL.md +43 -108
- package/keystroke-credential-binding/references/patterns.md +38 -66
- package/keystroke-credential-binding/references/source-map.md +9 -9
- package/keystroke-trigger-authoring/references/testing.md +2 -2
- package/keystroke-workflow-authoring/SKILL.md +1 -1
- package/keystroke-workflow-authoring/references/prebuilt-integrations.md +65 -65
- package/keystroke-workflow-authoring/references/runtime-helpers.md +3 -3
- package/keystroke-workflow-authoring/references/source-map.md +1 -1
- package/keystroke-workflow-authoring/references/testing.md +5 -4
- package/package.json +1 -1
|
@@ -95,7 +95,7 @@ Teach these rules:
|
|
|
95
95
|
|
|
96
96
|
Call out this distinction when it matters:
|
|
97
97
|
- `CredentialSet.id` is the raw authored id used in runtime credential context keys
|
|
98
|
-
- `
|
|
98
|
+
- `credentialDefinitionId` is the namespaced or manifest-facing id used for bindings and storage
|
|
99
99
|
- runtime lookups still use the raw `id`
|
|
100
100
|
|
|
101
101
|
This matters for integration authors and when explaining `createOperationFactory`.
|
|
@@ -118,14 +118,14 @@ This matters for integration authors and when explaining `createOperationFactory
|
|
|
118
118
|
- If `resolve` is present, `stored` must also be present.
|
|
119
119
|
- Use `CredentialSet` instead of env-based secret handling inside authored primitives.
|
|
120
120
|
- Follow Zod v4 syntax in examples and authored code. See `../../../.agents/rules/zod-v4-requirements.md`.
|
|
121
|
-
- The drawer-label rule is enforced at three layers today: compile-time on a single primitive (`AssertUniqueCredentialSetIds`), build-time across the project (`
|
|
121
|
+
- The drawer-label rule is enforced at three layers today: compile-time on a single primitive (`AssertUniqueCredentialSetIds`), build-time across the project (`assertUniqueCredentialDefinitionIds`), and deploy-time schema-drift detection (`detectSchemaDrifts`). Cluster 07 of the reconciled credential plan adds two more layers: construction-time identity registry and vault-row schema fingerprint. You do not need to do anything special in authored code beyond following the rule above.
|
|
122
122
|
|
|
123
123
|
### Vault-row schema mismatch
|
|
124
124
|
|
|
125
125
|
Every vault row is stamped with the credential set's schema fingerprint at upload. When a workflow later resolves credentials against a credential set whose `auth` or `stored` shape has changed since the row was written, the credential runtime raises `CredentialSchemaMismatchError` with a copy-paste remediation command:
|
|
126
126
|
|
|
127
127
|
```
|
|
128
|
-
The credentials stored for "
|
|
128
|
+
The credentials stored for "acme-crm" were uploaded against a different schema than the workflow currently expects.
|
|
129
129
|
|
|
130
130
|
Uploaded at: 2026-01-15T12:34:56.000Z
|
|
131
131
|
Uploaded shape: abc123…
|
|
@@ -139,133 +139,68 @@ The CLI (`keystroke credentials upload`) and the web UI (`POST /api/v1/credentia
|
|
|
139
139
|
|
|
140
140
|
You don't invoke the check yourself — it's wired into every resolver consumer. The only thing authored code does is what it already does: declare `auth` and `stored` honestly. When you change either, operators re-upload.
|
|
141
141
|
|
|
142
|
-
###
|
|
142
|
+
### Dynamic credential resolution
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
empty token produces a confusing 401 at the provider instead of a clear
|
|
148
|
-
"credentials not connected" error at the resolver boundary:
|
|
144
|
+
`CredentialSet` no longer accepts authored `resolve` hooks or separate
|
|
145
|
+
`stored` schemas. Runtime transforms that mint temporary credentials are modeled
|
|
146
|
+
as trusted dynamic connections:
|
|
149
147
|
|
|
150
148
|
```ts
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return { HUBSPOT_ACCESS_TOKEN: token };
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Pair the hook with a `.refine()` on the `stored` schema so Zod rejects
|
|
163
|
-
"neither field populated" at upload time, before the resolver runs.
|
|
164
|
-
|
|
165
|
-
For hooks that do real work (login exchange, KMS-signed JWT, ephemeral session
|
|
166
|
-
token), use the object form:
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
resolve: async (stored) => mintToken(stored),
|
|
170
|
-
resolveCacheMs: 10 * 60_000, // cache for 10 minutes within a single workflow run
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
A workflow run has a single cache; steps 2..N hit the cache. When the run
|
|
174
|
-
ends, the cache is discarded.
|
|
175
|
-
|
|
176
|
-
Pick `cacheMs` conservatively — under 75% of the provider's actual token
|
|
177
|
-
validity window is a safe default. If the cached token expires mid-run, pair
|
|
178
|
-
with top-level `onCredentialRevoked: 'retry-once'` on the credential set to self-heal.
|
|
179
|
-
|
|
180
|
-
### Self-healing with `retry-once`
|
|
181
|
-
|
|
182
|
-
When a step receives a 401 / 403 and the integration throws
|
|
183
|
-
`CredentialRevokedError`, the default behavior fails the step and marks the
|
|
184
|
-
connection broken. Sometimes the credential is actually fine — it just expired
|
|
185
|
-
a moment ago, and re-running `resolve` would produce a working token.
|
|
186
|
-
|
|
187
|
-
Opt in at the credential-set root (next to `connection`, not inside it):
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
onCredentialRevoked: 'retry-once',
|
|
191
|
-
connection: {
|
|
192
|
-
kind: 'manual',
|
|
193
|
-
}
|
|
149
|
+
connections: [
|
|
150
|
+
{
|
|
151
|
+
id: 'assume-role',
|
|
152
|
+
kind: 'dynamic',
|
|
153
|
+
resolver: { id: 'official.aws.assume-role', cacheMs: 10 * 60_000 },
|
|
154
|
+
},
|
|
155
|
+
]
|
|
194
156
|
```
|
|
195
157
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
status flips as today.
|
|
201
|
-
|
|
202
|
-
The revoke-retry is orthogonal to the normal retry budget: a step with
|
|
203
|
-
`retries: { attempts: 3 }` still gets its full three attempts after one
|
|
204
|
-
revoke-retry fires.
|
|
205
|
-
|
|
206
|
-
**When NOT to use.** For credential sets without a stateful `resolve` (Slack
|
|
207
|
-
bot tokens, pasted API keys), re-running `resolve` on the same stored values
|
|
208
|
-
produces the same output. `retry-once` would burn one extra attempt with the
|
|
209
|
-
same result. Leave the default `'fail'`.
|
|
158
|
+
The build/runtime contracts call this `needsDynamicResolution` with an optional
|
|
159
|
+
`dynamicResolutionCacheMs` hint. User-authored integrations should prefer
|
|
160
|
+
manual, OAuth, credentials-exchange, exchange, or platform connections unless a
|
|
161
|
+
trusted registered resolver already exists.
|
|
210
162
|
|
|
211
163
|
### Workspace-authored OAuth integrations
|
|
212
164
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
165
|
+
OAuth connections no longer declare where `clientId` / `clientSecret` come
|
|
166
|
+
from in authored code. Connection definitions are seeded or registered into
|
|
167
|
+
the database with an `oauth_client_policy`; trusted platform paths use that
|
|
168
|
+
policy to resolve the right provider app credential row.
|
|
217
169
|
|
|
218
|
-
The
|
|
219
|
-
`CredentialSet` primitive:
|
|
170
|
+
The final authoring shape stays focused on the user-runtime credential:
|
|
220
171
|
|
|
221
172
|
```ts
|
|
222
|
-
// file 1 — the internal client-app credential set
|
|
223
|
-
export const acmeClientApp = new CredentialSet({
|
|
224
|
-
id: 'acmeClientApp',
|
|
225
|
-
namespace: 'keystroke',
|
|
226
|
-
auth: z.object({ clientId: z.string(), clientSecret: z.string() }),
|
|
227
|
-
platformMetadata: { kind: 'provider-app', visibility: 'internal' },
|
|
228
|
-
// No `connection` — client-app credentials are provisioned out-of-band
|
|
229
|
-
// (operator env, admin upload), not through a user-facing connect flow.
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// file 2 — the user-connection OAuth credential set
|
|
233
173
|
export const acmeCrm = new CredentialSet({
|
|
234
174
|
id: 'acmeCrm',
|
|
235
|
-
namespace: 'keystroke',
|
|
236
|
-
platformMetadata: { kind: 'user-connection', visibility: 'user-visible' },
|
|
237
175
|
auth: z.object({ ACME_ACCESS_TOKEN: z.string() }),
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
credentialSet: acmeClientApp,
|
|
176
|
+
connections: [
|
|
177
|
+
{
|
|
178
|
+
id: 'oauth',
|
|
179
|
+
kind: 'oauth',
|
|
180
|
+
authUrl: 'https://auth.acme.example.com/oauth/authorize',
|
|
181
|
+
tokenUrl: 'https://auth.acme.example.com/oauth/token',
|
|
182
|
+
scopes: ['read:contacts'],
|
|
183
|
+
tokenType: 'refreshable',
|
|
184
|
+
vault: { accessToken: 'ACME_ACCESS_TOKEN' },
|
|
248
185
|
},
|
|
249
|
-
|
|
186
|
+
],
|
|
250
187
|
});
|
|
251
188
|
```
|
|
252
189
|
|
|
253
190
|
Key rules:
|
|
254
191
|
|
|
255
|
-
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
-
|
|
262
|
-
`appSecret` instead of `clientId` / `clientSecret`), pass a `keyMap`:
|
|
263
|
-
`oauthClientSource: { kind: 'workspace-provider-app', credentialSet: acmeClientApp, keyMap: { clientId: 'appId', clientSecret: 'appSecret' } }`.
|
|
192
|
+
- Provider app client material is represented by platform-only credential
|
|
193
|
+
definitions in DB seed/registration data, not by a public `CredentialSet`
|
|
194
|
+
field.
|
|
195
|
+
- `oauth_client_policy.kind = 'platform-provider-app'` selects Keystroke-owned
|
|
196
|
+
provider app credentials; `kind = 'workspace-provider-app'` selects
|
|
197
|
+
organization-scoped workspace provider app credentials.
|
|
198
|
+
- User-runtime consumers never receive platform-only provider app secrets.
|
|
264
199
|
|
|
265
200
|
The platform's initiate, callback, and refresh flows route workspace
|
|
266
201
|
OAuth through the same `resolveOAuthClient` helper first-party
|
|
267
202
|
integrations use — the end-to-end behavior is identical once the
|
|
268
|
-
workspace provider app is registered
|
|
203
|
+
workspace provider app is registered.
|
|
269
204
|
|
|
270
205
|
## Validating a credential at upload time
|
|
271
206
|
|
|
@@ -330,7 +265,7 @@ timeout for OAuth callbacks. Your hook should finish quickly; an
|
|
|
330
265
|
unresponsive provider endpoint surfaces to the operator as "Credential
|
|
331
266
|
validation timed out."
|
|
332
267
|
|
|
333
|
-
Canonical adopter:
|
|
268
|
+
Canonical adopter: the Stripe provider package in the integrations repo.
|
|
334
269
|
|
|
335
270
|
## `credentials-exchange`
|
|
336
271
|
|
|
@@ -346,7 +281,7 @@ not a third-party authorization server — exchange them for something else
|
|
|
346
281
|
Input: `username` / `password`. Stored: session cookie + Browserbase
|
|
347
282
|
context id. Rotate by probing the session and falling back to
|
|
348
283
|
`'needs-reinput'` when the cookie has expired. Use
|
|
349
|
-
`createBrowserLoginExchange` from `@
|
|
284
|
+
`createBrowserLoginExchange` from `@keystrokehq/browserbase`.
|
|
350
285
|
|
|
351
286
|
3. **Service-account JWT.** User pastes a private-key PEM. Input:
|
|
352
287
|
`clientEmail` + `privateKeyPem`. Stored: freshly-minted JWT. Rotate by
|
|
@@ -98,7 +98,7 @@ Use the declarative form when `tokenResult.accessToken` (optionally
|
|
|
98
98
|
|
|
99
99
|
```ts
|
|
100
100
|
import { CredentialSet } from '@keystrokehq/core';
|
|
101
|
-
import {
|
|
101
|
+
import { defineOAuthConnection } from '@keystroke/credential-connection';
|
|
102
102
|
import { z } from 'zod';
|
|
103
103
|
|
|
104
104
|
export const salesforceCredentials = new CredentialSet({
|
|
@@ -107,7 +107,7 @@ export const salesforceCredentials = new CredentialSet({
|
|
|
107
107
|
SALESFORCE_ACCESS_TOKEN: z.string(),
|
|
108
108
|
SALESFORCE_INSTANCE_URL: z.string(),
|
|
109
109
|
}),
|
|
110
|
-
connection:
|
|
110
|
+
connection: defineOAuthConnection({
|
|
111
111
|
authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
|
|
112
112
|
tokenUrl: 'https://login.salesforce.com/services/oauth2/token',
|
|
113
113
|
scopes: ['api', 'refresh_token', 'offline_access'],
|
|
@@ -137,7 +137,7 @@ Use the function form as the escape hatch when the vault layout depends on
|
|
|
137
137
|
derived values or on post-exchange userinfo stashed on `tokenResult.raw`.
|
|
138
138
|
|
|
139
139
|
```ts
|
|
140
|
-
connection:
|
|
140
|
+
connection: defineOAuthConnection({
|
|
141
141
|
authUrl: 'https://account.docusign.com/oauth/auth',
|
|
142
142
|
tokenUrl: 'https://account.docusign.com/oauth/token',
|
|
143
143
|
scopes: ['signature', 'extended'],
|
|
@@ -206,55 +206,33 @@ connection: {
|
|
|
206
206
|
},
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
Canonical adopter:
|
|
209
|
+
Canonical adopter: the Stripe provider package in the integrations repo.
|
|
210
210
|
|
|
211
|
-
##
|
|
211
|
+
## Dynamic credential resolution
|
|
212
212
|
|
|
213
|
-
Use
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
Use a trusted dynamic connection when a provider requires minting temporary
|
|
214
|
+
runtime credentials from stored connection state. The resolver is registered by
|
|
215
|
+
descriptor, not authored as a credential-set method.
|
|
216
216
|
|
|
217
217
|
```ts
|
|
218
218
|
import { CredentialSet } from '@keystrokehq/core';
|
|
219
|
-
import { CredentialRevokedError } from '@keystrokehq/core/errors';
|
|
220
219
|
import { z } from 'zod';
|
|
221
220
|
|
|
222
|
-
export const
|
|
223
|
-
id: '
|
|
224
|
-
auth: z.object({
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!res.ok) throw new CredentialRevokedError('legacyErp', 'Login rejected');
|
|
233
|
-
const { token } = (await res.json()) as { token: string };
|
|
234
|
-
return { sessionToken: token };
|
|
235
|
-
},
|
|
236
|
-
resolveCacheMs: 10 * 60_000, // tokens valid ~15min; refresh at 10min
|
|
237
|
-
onCredentialRevoked: 'retry-once',
|
|
238
|
-
connection: {
|
|
239
|
-
kind: 'manual',
|
|
240
|
-
instructions: 'Enter service-account credentials.',
|
|
241
|
-
},
|
|
221
|
+
export const aws = new CredentialSet({
|
|
222
|
+
id: 'aws',
|
|
223
|
+
auth: z.object({ AWS_ACCESS_KEY_ID: z.string(), AWS_SECRET_ACCESS_KEY: z.string() }),
|
|
224
|
+
connections: [
|
|
225
|
+
{
|
|
226
|
+
id: 'assume-role',
|
|
227
|
+
kind: 'dynamic',
|
|
228
|
+
resolver: { id: 'official.aws.assume-role', cacheMs: 10 * 60_000 },
|
|
229
|
+
},
|
|
230
|
+
],
|
|
242
231
|
});
|
|
243
232
|
```
|
|
244
233
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
onCredentialRevoked: 'retry-once',
|
|
249
|
-
connection: {
|
|
250
|
-
kind: 'manual',
|
|
251
|
-
instructions: 'Enter service-account credentials.',
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
Requires `resolve`. On `CredentialRevokedError`, the platform invalidates the
|
|
256
|
-
run's cache for this credential set, re-runs `resolve`, and retries the failing
|
|
257
|
-
step once. Orthogonal to the normal retry budget.
|
|
234
|
+
The generated contracts surface this as `needsDynamicResolution` and
|
|
235
|
+
`dynamicResolutionCacheMs`.
|
|
258
236
|
|
|
259
237
|
## `credentials-exchange` — API-login
|
|
260
238
|
|
|
@@ -314,7 +292,7 @@ export const acmeCrm = new CredentialSet({
|
|
|
314
292
|
## `credentials-exchange` — browser-login
|
|
315
293
|
|
|
316
294
|
Provider has no API; login requires a real browser. Use
|
|
317
|
-
`createBrowserLoginExchange` from `@
|
|
295
|
+
`createBrowserLoginExchange` from `@keystrokehq/browserbase` to
|
|
318
296
|
drive Browserbase + Stagehand. The LLM never sees plaintext credentials —
|
|
319
297
|
Stagehand substitutes `%fieldName%` at Chromium level.
|
|
320
298
|
|
|
@@ -322,7 +300,7 @@ Stagehand substitutes `%fieldName%` at Chromium level.
|
|
|
322
300
|
import {
|
|
323
301
|
createBrowserLoginExchange,
|
|
324
302
|
type BrowserbaseCredentials,
|
|
325
|
-
} from '@
|
|
303
|
+
} from '@keystrokehq/browserbase';
|
|
326
304
|
import { CredentialSet } from '@keystrokehq/core';
|
|
327
305
|
import { z } from 'zod';
|
|
328
306
|
|
|
@@ -464,8 +442,6 @@ import { z } from 'zod';
|
|
|
464
442
|
|
|
465
443
|
export const crmApi = new CredentialSet({
|
|
466
444
|
id: 'crmApi',
|
|
467
|
-
namespace: 'keystroke',
|
|
468
|
-
platformMetadata: { kind: 'user-connection', visibility: 'user-visible' },
|
|
469
445
|
auth: z.object({ apiKey: z.string() }),
|
|
470
446
|
stored: z.object({ vaultPath: z.string() }),
|
|
471
447
|
resolveAtPlatform: async (stored, ctx) => {
|
|
@@ -499,8 +475,6 @@ declare function assumeRole(params: {
|
|
|
499
475
|
|
|
500
476
|
export const awsAssumed = new CredentialSet({
|
|
501
477
|
id: 'awsAssumed',
|
|
502
|
-
namespace: 'keystroke',
|
|
503
|
-
platformMetadata: { kind: 'user-connection', visibility: 'user-visible' },
|
|
504
478
|
auth: z.object({
|
|
505
479
|
accessKeyId: z.string(),
|
|
506
480
|
secretAccessKey: z.string(),
|
|
@@ -541,8 +515,6 @@ declare function kmsSignJwt(params: {
|
|
|
541
515
|
|
|
542
516
|
export const serviceJwt = new CredentialSet({
|
|
543
517
|
id: 'serviceJwt',
|
|
544
|
-
namespace: 'keystroke',
|
|
545
|
-
platformMetadata: { kind: 'user-connection', visibility: 'user-visible' },
|
|
546
518
|
auth: z.object({ jwt: z.string() }),
|
|
547
519
|
stored: z.object({ kmsKeyArn: z.string(), issuer: z.string(), audience: z.string() }),
|
|
548
520
|
resolveAtPlatform: async (stored, ctx) => {
|
|
@@ -575,8 +547,7 @@ post-exchange identity fetches.
|
|
|
575
547
|
|
|
576
548
|
```ts
|
|
577
549
|
import {
|
|
578
|
-
|
|
579
|
-
exchangeCodeDefault,
|
|
550
|
+
oauthDefaults,
|
|
580
551
|
oauthPresets as p,
|
|
581
552
|
pipe,
|
|
582
553
|
} from '@keystroke/credential-connection';
|
|
@@ -584,8 +555,8 @@ import {
|
|
|
584
555
|
connection: {
|
|
585
556
|
kind: 'oauth',
|
|
586
557
|
// ...urls, scopes, tokenType, vault...
|
|
587
|
-
buildAuthUrl: pipe(
|
|
588
|
-
exchangeCode: pipe(
|
|
558
|
+
buildAuthUrl: pipe(oauthDefaults.buildAuthUrl, p.scopeDelimiter(',')),
|
|
559
|
+
exchangeCode: pipe(oauthDefaults.exchangeCode, p.requireSuccessFlag('ok')),
|
|
589
560
|
},
|
|
590
561
|
```
|
|
591
562
|
|
|
@@ -651,7 +622,7 @@ The same agent example could also be written with `new Operation({...})` if the
|
|
|
651
622
|
## Agent usage
|
|
652
623
|
|
|
653
624
|
```ts
|
|
654
|
-
import { anthropic } from '@
|
|
625
|
+
import { anthropic } from '@keystrokehq/ai';
|
|
655
626
|
import { Agent } from '@keystrokehq/core';
|
|
656
627
|
|
|
657
628
|
export const supportAgent = new Agent({
|
|
@@ -714,13 +685,14 @@ export const crmMcpServer = new McpServer({
|
|
|
714
685
|
|
|
715
686
|
Use `credentialMapper(...)` to convert Keystroke credential values into transport-specific config such as headers, env vars, or query parameters.
|
|
716
687
|
|
|
717
|
-
##
|
|
688
|
+
## Dynamic resolution flags
|
|
718
689
|
|
|
719
690
|
```ts
|
|
720
|
-
const
|
|
691
|
+
const usesDynamicResolution = requirement.needsDynamicResolution;
|
|
721
692
|
```
|
|
722
693
|
|
|
723
|
-
`
|
|
694
|
+
`needsDynamicResolution` is true only for requirements backed by a trusted
|
|
695
|
+
registered dynamic resolver.
|
|
724
696
|
|
|
725
697
|
## `describe()` and `toManifest()`
|
|
726
698
|
|
|
@@ -746,7 +718,7 @@ One `CredentialSet` declared in code is a **type**. The platform can store many
|
|
|
746
718
|
┌───────────────────────────────────────────────────────────────────┐
|
|
747
719
|
│ Credential Set TYPE │
|
|
748
720
|
│ Declared in code. One per integration. │
|
|
749
|
-
│ Example:
|
|
721
|
+
│ Example: gmail │
|
|
750
722
|
│ schema = { access_token: string, refresh_token: string } │
|
|
751
723
|
│ connection = OAuth 2.0 with Google │
|
|
752
724
|
└───────────────────────────────────────────────────────────────────┘
|
|
@@ -767,7 +739,7 @@ One `CredentialSet` declared in code is a **type**. The platform can store many
|
|
|
767
739
|
What authors often try first is the anti-pattern:
|
|
768
740
|
|
|
769
741
|
```ts
|
|
770
|
-
import { gmail } from '@
|
|
742
|
+
import { gmail } from '@keystrokehq/google';
|
|
771
743
|
import { Step } from '@keystrokehq/core';
|
|
772
744
|
|
|
773
745
|
export const brokenStep = new Step({
|
|
@@ -783,7 +755,7 @@ The supported path is **multi-step decomposition**: one credential set type, mul
|
|
|
783
755
|
|
|
784
756
|
```ts
|
|
785
757
|
// One credential set TYPE, declared exactly once (by the integration).
|
|
786
|
-
import { gmail } from '@
|
|
758
|
+
import { gmail } from '@keystrokehq/google';
|
|
787
759
|
import { Step, Workflow } from '@keystrokehq/core';
|
|
788
760
|
import { z } from 'zod';
|
|
789
761
|
|
|
@@ -833,14 +805,14 @@ export const inboxDigest = new Workflow({
|
|
|
833
805
|
});
|
|
834
806
|
```
|
|
835
807
|
|
|
836
|
-
At run time, the operator connects both Gmail accounts, which creates two vault rows under `
|
|
808
|
+
At run time, the operator connects both Gmail accounts, which creates two vault rows under `credentialDefinitionId: 'gmail'`, then chooses which one backs each call site:
|
|
837
809
|
|
|
838
810
|
```jsonc
|
|
839
811
|
{
|
|
840
812
|
"workflowId": "inbox-digest",
|
|
841
813
|
"credentialBindings": {
|
|
842
|
-
"readPersonal:
|
|
843
|
-
"readWork:
|
|
814
|
+
"readPersonal:gmail": "cset_personal",
|
|
815
|
+
"readWork:gmail": "cset_work"
|
|
844
816
|
}
|
|
845
817
|
}
|
|
846
818
|
```
|
|
@@ -856,14 +828,14 @@ One drawer per `id`, ever. A `CredentialSet` declaration is the label on the dra
|
|
|
856
828
|
When a collision is accidental, the platform is increasingly strict about catching it early:
|
|
857
829
|
|
|
858
830
|
- compile-time on a single primitive via `AssertUniqueCredentialSetIds`
|
|
859
|
-
- build-time across the project via `
|
|
831
|
+
- build-time across the project via `assertUniqueCredentialDefinitionIds`
|
|
860
832
|
- deploy-time via schema-drift detection
|
|
861
833
|
- later in the reconciled credential roadmap, construction-time identity registry and vault-row schema fingerprinting close the remaining gaps
|
|
862
834
|
|
|
863
835
|
When "same id" is actually a multi-instance requirement, the answer is still **one declaration, many connected rows**.
|
|
864
836
|
|
|
865
837
|
```ts
|
|
866
|
-
import { stripe } from '@
|
|
838
|
+
import { stripe } from '@keystrokehq/stripe';
|
|
867
839
|
|
|
868
840
|
// One declaration in code.
|
|
869
841
|
export const billingCredential = stripe;
|
|
@@ -10,25 +10,25 @@ import { CredentialSet } from '@keystrokehq/core';
|
|
|
10
10
|
|
|
11
11
|
- `id`
|
|
12
12
|
- `namespace`
|
|
13
|
-
- `
|
|
13
|
+
- `credentialDefinitionId`
|
|
14
14
|
- `name`
|
|
15
15
|
- `description`
|
|
16
16
|
- `auth`
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
17
|
+
- `connections`
|
|
18
|
+
- `needsRawSecret`
|
|
19
|
+
- `onCredentialRevoked`
|
|
20
20
|
|
|
21
21
|
### What they are used for
|
|
22
22
|
|
|
23
23
|
- `id`: stable credential set identifier
|
|
24
24
|
- `namespace`: optional namespace used for manifest and storage identity
|
|
25
|
-
- `
|
|
25
|
+
- `credentialDefinitionId`: computed namespaced id used for manifest and binding infrastructure
|
|
26
26
|
- `name`: human-readable name
|
|
27
27
|
- `description`: short explanation of what the credentials are for
|
|
28
28
|
- `auth`: runtime credential schema
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
29
|
+
- `connections`: credential acquisition paths such as manual, OAuth, exchange, dynamic, or platform
|
|
30
|
+
- `needsRawSecret`: routes real secret values into the runtime when proxy substitution cannot work
|
|
31
|
+
- `onCredentialRevoked`: top-level policy for revoked credential errors
|
|
32
32
|
|
|
33
33
|
## `CredentialSet` instance methods
|
|
34
34
|
|
|
@@ -61,7 +61,7 @@ import { CredentialSet } from '@keystrokehq/core';
|
|
|
61
61
|
- if `stored` is present, `resolve` must also be present
|
|
62
62
|
- if `resolve` is present, `stored` must also be present
|
|
63
63
|
- prefer typed access through runtime context rather than ad hoc env access inside primitives
|
|
64
|
-
- runtime credential context keys use raw `id`, not `
|
|
64
|
+
- runtime credential context keys use raw `id`, not `credentialDefinitionId`
|
|
65
65
|
|
|
66
66
|
## Where to read next
|
|
67
67
|
|
|
@@ -6,11 +6,11 @@ Assume `paymentWebhook`, `orderPolling`, and `paymentWorkflow` are the public tr
|
|
|
6
6
|
|
|
7
7
|
## Vitest setup
|
|
8
8
|
|
|
9
|
-
`keystrokeTestPlugin()` adds the
|
|
9
|
+
`keystrokeTestPlugin()` adds the Keystroke testing setup file to Vitest, which is required for credential resolution and context mocking.
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
import { defineConfig } from 'vitest/config';
|
|
13
|
-
import { keystrokeTestPlugin } from '@keystrokehq/
|
|
13
|
+
import { keystrokeTestPlugin } from '@keystrokehq/testing/vitest';
|
|
14
14
|
|
|
15
15
|
export default defineConfig({
|
|
16
16
|
plugins: [keystrokeTestPlugin()],
|
|
@@ -123,7 +123,7 @@ Teach these rules:
|
|
|
123
123
|
- waits and hooks
|
|
124
124
|
5. Put operational work in steps, shared operations, or agents.
|
|
125
125
|
6. Keep each exported primitive in its own typed file.
|
|
126
|
-
7. Finish with tests using `@keystrokehq/
|
|
126
|
+
7. Finish with tests using `@keystrokehq/testing/vitest`.
|
|
127
127
|
|
|
128
128
|
## Workflow rules
|
|
129
129
|
|