@keystrokehq/skills 0.0.1

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.
Files changed (44) hide show
  1. package/AGENTS-blurb.md +123 -0
  2. package/LICENSE +21 -0
  3. package/README.md +63 -0
  4. package/keystroke-agent-authoring/SKILL.md +225 -0
  5. package/keystroke-agent-authoring/evals/evals.json +29 -0
  6. package/keystroke-agent-authoring/references/messaging-gateways.md +242 -0
  7. package/keystroke-agent-authoring/references/patterns.md +417 -0
  8. package/keystroke-agent-authoring/references/prebuilt-integrations.md +879 -0
  9. package/keystroke-agent-authoring/references/sandbox-and-mcp.md +214 -0
  10. package/keystroke-agent-authoring/references/source-map.md +182 -0
  11. package/keystroke-agent-authoring/references/testing.md +85 -0
  12. package/keystroke-cli-workspace/SKILL.md +93 -0
  13. package/keystroke-cli-workspace/evals/evals.json +23 -0
  14. package/keystroke-cli-workspace/references/command-map.md +50 -0
  15. package/keystroke-cli-workspace/references/credentials-and-connect.md +79 -0
  16. package/keystroke-cli-workspace/references/project-lifecycle.md +85 -0
  17. package/keystroke-credential-binding/SKILL.md +509 -0
  18. package/keystroke-credential-binding/evals/evals.json +29 -0
  19. package/keystroke-credential-binding/references/cli.md +85 -0
  20. package/keystroke-credential-binding/references/patterns.md +878 -0
  21. package/keystroke-credential-binding/references/source-map.md +69 -0
  22. package/keystroke-data-toolkit/SKILL.md +59 -0
  23. package/keystroke-data-toolkit/evals/evals.json +23 -0
  24. package/keystroke-data-toolkit/references/usage.md +79 -0
  25. package/keystroke-task-authoring/SKILL.md +124 -0
  26. package/keystroke-task-authoring/evals/evals.json +23 -0
  27. package/keystroke-task-authoring/references/patterns.md +132 -0
  28. package/keystroke-task-authoring/references/source-map.md +61 -0
  29. package/keystroke-trigger-authoring/SKILL.md +189 -0
  30. package/keystroke-trigger-authoring/evals/evals.json +29 -0
  31. package/keystroke-trigger-authoring/references/patterns.md +265 -0
  32. package/keystroke-trigger-authoring/references/source-map.md +128 -0
  33. package/keystroke-trigger-authoring/references/testing.md +148 -0
  34. package/keystroke-workflow-as-tool-debugging/SKILL.md +52 -0
  35. package/keystroke-workflow-as-tool-debugging/evals/evals.json +23 -0
  36. package/keystroke-workflow-as-tool-debugging/references/playbook.md +77 -0
  37. package/keystroke-workflow-authoring/SKILL.md +234 -0
  38. package/keystroke-workflow-authoring/evals/evals.json +29 -0
  39. package/keystroke-workflow-authoring/references/patterns.md +265 -0
  40. package/keystroke-workflow-authoring/references/prebuilt-integrations.md +811 -0
  41. package/keystroke-workflow-authoring/references/runtime-helpers.md +264 -0
  42. package/keystroke-workflow-authoring/references/source-map.md +108 -0
  43. package/keystroke-workflow-authoring/references/testing.md +108 -0
  44. package/package.json +26 -0
@@ -0,0 +1,79 @@
1
+ # Credentials And Connect
2
+
3
+ Read this file when the user needs Keystroke CLI guidance for integration connection or credential upload.
4
+
5
+ ## Connect an official integration
6
+
7
+ Inspect first:
8
+
9
+ ```bash
10
+ keystroke connect --help
11
+ ```
12
+
13
+ Then connect:
14
+
15
+ ```bash
16
+ keystroke connect slack
17
+ keystroke connect github
18
+ keystroke connect linear
19
+ ```
20
+
21
+ Use `connect` for official Keystroke-managed connection flows.
22
+
23
+ ## Inspect credential requirements
24
+
25
+ Inspect first:
26
+
27
+ ```bash
28
+ keystroke credentials requirements --help
29
+ ```
30
+
31
+ Then inspect:
32
+
33
+ ```bash
34
+ keystroke credentials requirements --path <project-dir> --json
35
+ ```
36
+
37
+ ## Upload official integration credentials
38
+
39
+ Inspect first:
40
+
41
+ ```bash
42
+ keystroke credentials upload --help
43
+ ```
44
+
45
+ Then upload:
46
+
47
+ ```bash
48
+ keystroke credentials upload --integration slack --path <project-dir> --json
49
+ keystroke credentials upload --integration github --path <project-dir> --json
50
+ ```
51
+
52
+ ## Upload a custom credential set
53
+
54
+ ```bash
55
+ keystroke credentials upload --credential-set <id> --keys KEY_ONE,KEY_TWO --scope user --json
56
+ ```
57
+
58
+ ## Env mapping
59
+
60
+ If the expected keys are:
61
+ - `API_KEY`
62
+ - `ACCOUNT_ID`
63
+
64
+ then the env vars should be:
65
+ - `KEYSTROKE_API_KEY`
66
+ - `KEYSTROKE_ACCOUNT_ID`
67
+
68
+ Example:
69
+
70
+ ```bash
71
+ KEYSTROKE_API_KEY=secret KEYSTROKE_ACCOUNT_ID=acct_123 \
72
+ keystroke credentials upload --credential-set billingApi --keys API_KEY,ACCOUNT_ID --scope organization --json
73
+ ```
74
+
75
+ ## Gotchas
76
+
77
+ - Inspect `--help` before using credential commands.
78
+ - Use `connect` for connection flows and `credentials upload` for upload flows.
79
+ - Do not teach stale flags such as `--from-workflows` or `--no-verify`.
@@ -0,0 +1,85 @@
1
+ # CLI Project Lifecycle
2
+
3
+ Read this file when the user needs a practical command flow for authoring and operating a Keystroke project.
4
+
5
+ ## Initialize a project
6
+
7
+ Inspect first:
8
+
9
+ ```bash
10
+ keystroke init --help
11
+ ```
12
+
13
+ Then initialize:
14
+
15
+ ```bash
16
+ keystroke init
17
+ ```
18
+
19
+ ## Build workflows
20
+
21
+ Inspect first:
22
+
23
+ ```bash
24
+ keystroke workflows build --help
25
+ ```
26
+
27
+ Then build:
28
+
29
+ ```bash
30
+ keystroke workflows build
31
+ ```
32
+
33
+ ## Validate workflows
34
+
35
+ Inspect first:
36
+
37
+ ```bash
38
+ keystroke workflows validate --help
39
+ ```
40
+
41
+ Then validate:
42
+
43
+ ```bash
44
+ keystroke workflows validate
45
+ ```
46
+
47
+ ## Inspect or diff workflows
48
+
49
+ ```bash
50
+ keystroke workflows inspect --help
51
+ keystroke workflows diff --help
52
+ ```
53
+
54
+ ## Inspect workflow env or logs
55
+
56
+ ```bash
57
+ keystroke workflows env --help
58
+ keystroke workflows logs --help
59
+ keystroke workflows paused --help
60
+ ```
61
+
62
+ ## Deploy workflows
63
+
64
+ ```bash
65
+ keystroke deploy --help
66
+ keystroke deploy
67
+ ```
68
+
69
+ ## Deploy tasks
70
+
71
+ ```bash
72
+ keystroke deploy --help
73
+ keystroke deploy --target path/to/task.task.ts
74
+ ```
75
+
76
+ Use `deploy --target <task.task.ts>` when the project change is focused on an authored `Task` primitive rather than a full project deploy.
77
+
78
+ ## Sync Keystroke skills
79
+
80
+ ```bash
81
+ keystroke skills sync --help
82
+ keystroke skills sync
83
+ ```
84
+
85
+ Use this when the local project should copy packaged Keystroke skills into `.cursor/skills` and `.claude/skills`.
@@ -0,0 +1,509 @@
1
+ ---
2
+ name: keystroke-credential-binding
3
+ description: Design and bind Keystroke credential sets across steps, tools, agents, tasks, triggers, gateways, and MCP servers. Use when the user needs to define a CredentialSet, wire credentials into authored primitives, inspect credential requirements, or upload credentials through the Keystroke CLI.
4
+ ---
5
+
6
+ # Keystroke Credential Binding
7
+
8
+ Use this skill when an agent needs to define, attach, inspect, or upload Keystroke credentials.
9
+
10
+ This is the deep-dive credential skill. Workflow, agent, task, and trigger skills should link here instead of re-explaining the full credential story.
11
+
12
+ ## Quick start
13
+
14
+ `crm-api.credential-set.ts`
15
+
16
+ ```ts
17
+ import { CredentialSet } from '@keystrokehq/core';
18
+ import { z } from 'zod';
19
+
20
+ export const crmCredentials = new CredentialSet({
21
+ id: 'crmApi',
22
+ name: 'CRM API',
23
+ auth: z.object({
24
+ apiKey: z.string(),
25
+ }),
26
+ });
27
+ ```
28
+
29
+ `load-customer.step.ts`
30
+
31
+ ```ts
32
+ import { Step } from '@keystrokehq/core';
33
+ import { z } from 'zod';
34
+ import { crmCredentials } from './crm-api.credential-set';
35
+
36
+ export const loadCustomer = new Step({
37
+ name: 'Load Customer',
38
+ description: 'Uses a typed credential set from step context.',
39
+ credentialSets: [crmCredentials],
40
+ input: z.object({
41
+ customerId: z.string(),
42
+ }),
43
+ output: z.object({
44
+ customerId: z.string(),
45
+ keyUsed: z.string(),
46
+ }),
47
+ run: async (input, ctx) => ({
48
+ customerId: input.customerId,
49
+ keyUsed: ctx.credentials.crmApi.apiKey,
50
+ }),
51
+ });
52
+ ```
53
+
54
+ ## The drawer-label rule
55
+
56
+ A `CredentialSet`'s `id` is a **label on a drawer in the vault**. Exactly one drawer per label, ever. Declaring `new CredentialSet({ id: 'stripe', ... })` twice does not make two drawers. Both references point at the same drawer, so whoever writes last wins and whoever reads gets whatever happens to be inside.
57
+
58
+ The `auth` schema is the **contract on that drawer's contents**. If the contract changes and the drawer is not refilled to match, the next reader fails at Zod parse.
59
+
60
+ Three consequences every author needs to know:
61
+
62
+ 1. Do not declare the same `id` in two files expecting two drawers. Either import the shared instance from one module or pick distinct ids.
63
+ 2. For "I want two of the same integration" situations (two HubSpot portals, two Slack workspaces), declare the credential set **once** and connect as many times as you need. The vault supports many rows per credential set. Per-step `credentialBindings` picks which row backs which step at run time. See [`references/patterns.md` §"Type vs instance: one `CredentialSet`, many connected accounts"`](./references/patterns.md#type-vs-instance-one-credentialset-many-connected-accounts).
64
+ 3. For "this provider exposes two authorization modalities" situations (OAuth vs pasted private-app token, bot token vs user token, live vs test key), declare the credential set **once** with a `stored` schema that accepts either input shape and a `resolve` hook that collapses them into a single runtime `auth` value. Splitting by modality would force every consuming step, tool, agent, and template to branch on which drawer is populated — leaking an authorization-modality detail that the provider's API itself does not expose. The HubSpot integration is the canonical example: `stored` accepts `ACCESS_TOKEN` (OAuth) or `API_KEY` (private app); `resolve` normalizes both into `{ HUBSPOT_ACCESS_TOKEN }`. Only split when the runtime shape, granted scopes, or target endpoints genuinely differ between modalities (e.g. HubSpot's separate `hubspot-developer` drawer for developer-app-scoped APIs that cannot accept a user access token).
65
+
66
+ ## Authoring model
67
+
68
+ Teach this mental model clearly:
69
+ - a `CredentialSet` defines typed auth values in code
70
+ - authored primitives attach credential sets where they need them
71
+ - runtime code reads credentials through typed context, not through `process.env`
72
+ - upload and connect flows satisfy the authored credential requirements before deployment or execution
73
+
74
+ ## Where credentials can be attached
75
+
76
+ Credential sets can show up on:
77
+ - steps and tools
78
+ - agents
79
+ - tasks indirectly through the agent they reference
80
+ - triggers
81
+ - messaging gateways
82
+ - MCP servers
83
+
84
+ Do not assume one credential location covers every layer. An agent can need provider credentials, tool credentials, gateway credentials, and MCP credentials at the same time.
85
+
86
+ ## Runtime access
87
+
88
+ Teach these rules:
89
+ - steps and tools read credentials from `ctx.credentials`
90
+ - only the trigger `verify` callback receives credentials through its callback context (`filter`, `idempotencyKey`, and `transform` do not receive credentials)
91
+ - MCP servers map credentials through `credentialMapper`
92
+ - authored code should not read secrets from `process.env`
93
+
94
+ ## Important id rule
95
+
96
+ Call out this distinction when it matters:
97
+ - `CredentialSet.id` is the raw authored id used in runtime credential context keys
98
+ - `resolvedCredentialSetId` is the namespaced or manifest-facing id used for bindings and storage
99
+ - runtime lookups still use the raw `id`
100
+
101
+ This matters for integration authors and when explaining `createOperationFactory`.
102
+
103
+ ## Default credential process
104
+
105
+ 1. Group related auth values into a single credential set.
106
+ 2. Reuse an existing credential set when it already fits.
107
+ 3. Define the `CredentialSet` with `auth`, or with `stored` plus `resolve` when runtime auth must be derived.
108
+ 4. Attach the credential set to the primitive that needs it.
109
+ 5. Read credentials through typed runtime context.
110
+ 6. Inspect requirements before deploy.
111
+ 7. Upload or connect credentials through the CLI only when the user wants that step performed.
112
+
113
+ ## Credential rules
114
+
115
+ - Keep each exported credential set in its own `*.credential-set.ts` file.
116
+ - Use `auth` for direct runtime credential shapes.
117
+ - If `stored` is present, `resolve` must also be present.
118
+ - If `resolve` is present, `stored` must also be present.
119
+ - Use `CredentialSet` instead of env-based secret handling inside authored primitives.
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 (`assertUniqueCredentialSetResolvedIds`), 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
+
123
+ ### Vault-row schema mismatch
124
+
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
+
127
+ ```
128
+ The credentials stored for "keystroke:acme-crm" were uploaded against a different schema than the workflow currently expects.
129
+
130
+ Uploaded at: 2026-01-15T12:34:56.000Z
131
+ Uploaded shape: abc123…
132
+ Current shape: def456…
133
+
134
+ To fix, re-upload credentials:
135
+ keystroke credentials upload --credential-set acme-crm --keys sessionToken,refreshToken
136
+ ```
137
+
138
+ The CLI (`keystroke credentials upload`) and the web UI (`POST /api/v1/credentials/resolve`'s 422 response) both render this command verbatim. Preflight surfaces the same mismatch under `schemaMismatches[]` before any step dispatches, so operators don't hit the failure mid-run. Rotations against a stale row are skipped (the credential set is marked `needs_reconnect` until re-uploaded).
139
+
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
+
142
+ ### Caching `resolve`
143
+
144
+ By default every step calls your `resolve` hook fresh. For shape-normalizing
145
+ hooks (HubSpot, Notion) that is cheap and leaves authors on the plain-function
146
+ form. Throw on missing input rather than falling back to an empty string — an
147
+ empty token produces a confusing 401 at the provider instead of a clear
148
+ "credentials not connected" error at the resolver boundary:
149
+
150
+ ```ts
151
+ resolve: async (stored) => {
152
+ const token = stored.ACCESS_TOKEN ?? stored.API_KEY;
153
+ if (!token) {
154
+ throw new Error(
155
+ 'Unable to resolve HubSpot credentials: provide either ACCESS_TOKEN (via OAuth) or API_KEY (private app)'
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
+ }
194
+ ```
195
+
196
+ Requires `resolve`. On `CredentialRevokedError`, the platform invalidates the
197
+ run's cache for this credential set, re-runs `resolve`, and retries the step
198
+ once. If the retry succeeds, the workflow continues and the connection is not
199
+ marked broken. If the retry fails, the step fails normally and the connection
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'`.
210
+
211
+ ### Workspace-authored OAuth integrations
212
+
213
+ First-party (`namespace: 'keystroke'`) OAuth credential sets source
214
+ `clientId` / `clientSecret` from the platform's shared provider apps.
215
+ User-authored OAuth credential sets need to point at their own OAuth app,
216
+ so they MUST declare `oauthClientSource` — construction fails otherwise.
217
+
218
+ The pattern is two credential sets, both authored with the raw
219
+ `CredentialSet` primitive:
220
+
221
+ ```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
+ export const acmeCrm = new CredentialSet({
234
+ id: 'acmeCrm',
235
+ namespace: 'keystroke',
236
+ platformMetadata: { kind: 'user-connection', visibility: 'user-visible' },
237
+ auth: z.object({ ACME_ACCESS_TOKEN: z.string() }),
238
+ connection: {
239
+ kind: 'oauth',
240
+ authUrl: 'https://auth.acme.example.com/oauth/authorize',
241
+ tokenUrl: 'https://auth.acme.example.com/oauth/token',
242
+ scopes: ['read:contacts'],
243
+ tokenType: 'refreshable',
244
+ vault: { accessToken: 'ACME_ACCESS_TOKEN' },
245
+ oauthClientSource: {
246
+ kind: 'workspace-provider-app',
247
+ credentialSet: acmeClientApp,
248
+ },
249
+ },
250
+ });
251
+ ```
252
+
253
+ Key rules:
254
+
255
+ - The `clientApp` MUST be marked `platformMetadata: { kind: 'provider-app', visibility: 'internal' }`
256
+ so the sandbox refuses to inject `clientSecret` into user step code.
257
+ Construction of the user-connection set throws if its
258
+ `oauthClientSource.credentialSet` points at a non-internal set.
259
+ - User-namespaced OAuth MUST declare `oauthClientSource`. Construction
260
+ throws with a clear error if it's missing.
261
+ - If the `clientApp` schema uses alternate key names (e.g. `appId` /
262
+ `appSecret` instead of `clientId` / `clientSecret`), pass a `keyMap`:
263
+ `oauthClientSource: { kind: 'workspace-provider-app', credentialSet: acmeClientApp, keyMap: { clientId: 'appId', clientSecret: 'appSecret' } }`.
264
+
265
+ The platform's initiate, callback, and refresh flows route workspace
266
+ OAuth through the same `resolveOAuthClient` helper first-party
267
+ integrations use — the end-to-end behavior is identical once the
268
+ workspace provider app is registered (`keystroke integrations register <id> --client-app <credentialSetId>`).
269
+
270
+ ## Validating a credential at upload time
271
+
272
+ Every `CredentialSet.connection` accepts an optional `validate` hook. It runs
273
+ after Zod parses the submitted values and before the vault write commits.
274
+ Throw with a helpful message to reject; the thrown message is shown verbatim
275
+ to the operator in both the CLI and the OAuth callback UI.
276
+
277
+ ```ts
278
+ // Manual
279
+ connection: {
280
+ kind: 'manual',
281
+ instructions: '<where does the operator find this credential?>',
282
+ validate: async ({ credentials }) => {
283
+ const res = await fetch('<provider /me or /account endpoint>', {
284
+ headers: { Authorization: `Bearer ${credentials.AUTH_KEY}` },
285
+ });
286
+ if (!res.ok) {
287
+ throw new Error(
288
+ '<provider> rejected this credential. ' +
289
+ '<one actionable sentence about what to check>'
290
+ );
291
+ }
292
+ },
293
+ }
294
+
295
+ // OAuth — scope-delta check
296
+ validate: async ({ grantedScopes, requestedScopes }) => {
297
+ const missing = requestedScopes.filter((s) => !grantedScopes.includes(s));
298
+ if (missing.length > 0) {
299
+ throw new Error(
300
+ `<provider> install is missing required scopes: ${missing.join(', ')}. ` +
301
+ 'Reinstall the app and grant all requested permissions.'
302
+ );
303
+ }
304
+ },
305
+ ```
306
+
307
+ ### When to use `validate`
308
+
309
+ Reach for it when:
310
+
311
+ - The provider has a cheap `whoami` / `/me` / `/account` endpoint.
312
+ - Rejected credentials would otherwise surface at first workflow run.
313
+ - Typoed keys, wrong-mode keys, revoked tokens, or missing OAuth scopes are
314
+ common failure modes.
315
+
316
+ ### When NOT to use `validate`
317
+
318
+ Skip it when:
319
+
320
+ - The provider does not offer a cheap validation endpoint.
321
+ - Rate limits make validate calls expensive.
322
+ - The credential can only be validated mid-workflow (e.g. per-operation
323
+ signing). Runtime 401 self-heal lives on a separate primitive
324
+ (`onCredentialRevoked`; see cluster 09 of the reconciled credential plan).
325
+
326
+ ### Timeouts
327
+
328
+ The platform enforces an 8-second timeout for CLI uploads and a 6-second
329
+ timeout for OAuth callbacks. Your hook should finish quickly; an
330
+ unresponsive provider endpoint surfaces to the operator as "Credential
331
+ validation timed out."
332
+
333
+ Canonical adopter: `packages/integrations/integration-stripe/src/integration.ts`.
334
+
335
+ ## `credentials-exchange`
336
+
337
+ Use `kind: 'credentials-exchange'` when a user submits credentials and *you* —
338
+ not a third-party authorization server — exchange them for something else
339
+ (session token, cookie, JWT). Three common shapes:
340
+
341
+ 1. **API-login.** Provider has a `POST /login` endpoint. Input: `username` /
342
+ `password`. Stored: session token + refresh token. Rotate via the refresh
343
+ endpoint. Expiry comes from the provider's TTL.
344
+
345
+ 2. **Browser-login.** Provider has no API; login requires a real browser.
346
+ Input: `username` / `password`. Stored: session cookie + Browserbase
347
+ context id. Rotate by probing the session and falling back to
348
+ `'needs-reinput'` when the cookie has expired. Use
349
+ `createBrowserLoginExchange` from `@keystroke/integration-browserbase`.
350
+
351
+ 3. **Service-account JWT.** User pastes a private-key PEM. Input:
352
+ `clientEmail` + `privateKeyPem`. Stored: freshly-minted JWT. Rotate by
353
+ re-minting from the PEM (if you keep the PEM in `stored`) or by
354
+ returning `'needs-reinput'` (if you discard the PEM after first mint).
355
+
356
+ ```ts
357
+ connection: {
358
+ kind: 'credentials-exchange',
359
+ input: z.object({ username: z.email(), password: z.string().min(1) }),
360
+ exchange: async ({ input }) => {
361
+ const res = await fetch('https://acme.example.com/auth/login', {
362
+ method: 'POST',
363
+ headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify(input),
365
+ });
366
+ if (res.status === 401) {
367
+ return { status: 'needs-reinput', message: 'Invalid credentials.' };
368
+ }
369
+ if (!res.ok) throw new Error(`Login failed: ${res.status}`);
370
+ const body = await res.json();
371
+ return {
372
+ status: 'exchanged',
373
+ stored: { sessionToken: body.token, refreshToken: body.refresh },
374
+ expiresAt: new Date(Date.now() + body.ttlSec * 1000),
375
+ };
376
+ },
377
+ rotate: async ({ previous }) => {
378
+ const res = await fetch('https://acme.example.com/auth/refresh', {
379
+ method: 'POST',
380
+ body: JSON.stringify({ refresh: previous.refreshToken }),
381
+ });
382
+ if (!res.ok) return { status: 'needs-reinput' };
383
+ const body = await res.json();
384
+ return {
385
+ status: 'exchanged',
386
+ stored: { sessionToken: body.token, refreshToken: body.refresh },
387
+ expiresAt: new Date(Date.now() + body.ttlSec * 1000),
388
+ };
389
+ },
390
+ }
391
+ ```
392
+
393
+ ### When to use
394
+
395
+ Reach for `credentials-exchange` when:
396
+
397
+ - The user inputs one thing and the vault stores a different thing.
398
+ - There is a rotation lifecycle (TTL, refresh token, session renewal).
399
+ - You want the platform to manage rotate cadence rather than re-running
400
+ the exchange per step.
401
+
402
+ ### When NOT to use
403
+
404
+ Stick with `kind: 'manual'` + `stored` + `resolve` when:
405
+
406
+ - What the user types is what you store (Stripe: paste secret key, store
407
+ secret key).
408
+ - There is no rotation. (API keys that don't expire.)
409
+ - The transform from `stored` to `auth` is pure and in-process (no
410
+ network).
411
+
412
+ ### When to use `kind: 'manual'` with a `resolve` hook instead
413
+
414
+ `kind: 'manual'` + `stored` + `resolve` is still valid for non-lifecycle
415
+ transforms: pure shape mapping or auth-modality normalization where no
416
+ server handshake or rotation is involved. The HubSpot integration uses this
417
+ pattern to collapse two authorization modalities (OAuth `ACCESS_TOKEN` or
418
+ private-app `API_KEY`) into a single runtime `HUBSPOT_ACCESS_TOKEN` — note
419
+ that HubSpot's `connection.kind` is `'oauth'` for the connect flow, but the
420
+ `stored`/`resolve` pair is what lets a user skip OAuth and paste a
421
+ private-app token via `keystroke credentials upload --credential-set hubspot
422
+ --keys API_KEY=…` instead.
423
+
424
+ Pick `credentials-exchange` only when the transform involves a server-side
425
+ handshake and the resulting token has a lifecycle.
426
+
427
+ ### `'needs-reinput'` semantics
428
+
429
+ `CredentialsExchangeResult` is a discriminated union. The non-success
430
+ branch is `{ status: 'needs-reinput', message? }` — **not** a boolean.
431
+ Returning it:
432
+
433
+ - Preserves the prior vault row (if any) so the reconnect flow has
434
+ `previous` to reference.
435
+ - Surfaces `message` verbatim to the CLI / web UI.
436
+ - Marks the credential set `needs_reconnect` when it fires from the
437
+ rotate scheduler.
438
+
439
+ ## `resolveAtPlatform`
440
+
441
+ Use `resolveAtPlatform` instead of `resolve` when the transform needs
442
+ trusted network access (external secrets manager, STS `AssumeRole`,
443
+ KMS / HSM signing) or platform env vars that must not be visible to
444
+ user step code. The hook runs on the executor with a scoped `fetch`
445
+ and an allowlisted `env`; the resolved `auth` values reach the sandbox
446
+ but the stored inputs (vault path, platform IAM credentials) never do.
447
+
448
+ ```ts
449
+ export const crmApi = new CredentialSet({
450
+ id: 'crmApi',
451
+ auth: z.object({ apiKey: z.string() }),
452
+ stored: z.object({ vaultPath: z.string() }),
453
+ resolveAtPlatform: async (stored, ctx) => {
454
+ const res = await ctx.fetch(`https://vault.internal/v1/${stored.vaultPath}`, {
455
+ headers: { 'X-Vault-Token': ctx.env.VAULT_TOKEN ?? '' },
456
+ });
457
+ const { data } = (await res.json()) as { data: { apiKey: string } };
458
+ return { apiKey: data.apiKey };
459
+ },
460
+ platformEnvAllowlist: ['VAULT_TOKEN'],
461
+ });
462
+ ```
463
+
464
+ **Reference Keystroke-official only in v1.** User-authored
465
+ `resolveAtPlatform` is rejected at the runtime adapter boundary because
466
+ the hook runs with unscoped network access on the trusted host. Add a
467
+ new ADR before lifting this restriction.
468
+
469
+ ### `resolve` vs `resolveAtPlatform` vs neither
470
+
471
+ - **Use `resolve`** when the transform is pure shape / mapping on
472
+ already-decrypted vault values — no network call, no platform env.
473
+ *Example:* HubSpot's `stored.ACCESS_TOKEN ?? stored.API_KEY`
474
+ modality-normalization hook.
475
+ - **Use `resolveAtPlatform`** when the transform must call a service
476
+ the sandbox may not reach (HashiCorp Vault, AWS STS, GCP KMS,
477
+ 1Password Connect) or read platform env vars the sandbox may not
478
+ see. The actual secret lives in the external manager; the Keystroke
479
+ vault holds only a reference. *Example:* AWS STS `AssumeRole` mints
480
+ 15-minute per-run credentials.
481
+ - **Use neither** when the stored values are already what steps need.
482
+ Most integrations fall here (Slack, GitHub, Stripe).
483
+
484
+ See `references/patterns.md` for three worked examples (Vault, STS
485
+ `AssumeRole`, KMS-signed JWT).
486
+
487
+ ## CLI flows to teach
488
+
489
+ Teach only the current command surface:
490
+ - `keystroke credentials list`
491
+ - `keystroke credentials requirements --path <dir> --json`
492
+ - `keystroke credentials upload --integration <public-id> --path <dir> --json`
493
+ - `keystroke credentials upload --credential-set <id> --keys <KEY1,KEY2> --scope <scope> --json`
494
+
495
+ When the user has env-backed values, explain the `KEYSTROKE_<KEY>` convention.
496
+
497
+ Do not teach stale flows such as:
498
+ - `--from-workflows`
499
+ - `--no-verify`
500
+ - invented commands like `credentials add`
501
+
502
+ For wider command usage, cross-link to `../keystroke-cli-workspace/SKILL.md`.
503
+
504
+ ## References
505
+
506
+ Read these files as needed:
507
+ - `references/source-map.md` for the public credential surface
508
+ - `references/cli.md` for the current credential command cookbook
509
+ - `references/patterns.md` for field-by-field code examples