@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.
- package/AGENTS-blurb.md +123 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/keystroke-agent-authoring/SKILL.md +225 -0
- package/keystroke-agent-authoring/evals/evals.json +29 -0
- package/keystroke-agent-authoring/references/messaging-gateways.md +242 -0
- package/keystroke-agent-authoring/references/patterns.md +417 -0
- package/keystroke-agent-authoring/references/prebuilt-integrations.md +879 -0
- package/keystroke-agent-authoring/references/sandbox-and-mcp.md +214 -0
- package/keystroke-agent-authoring/references/source-map.md +182 -0
- package/keystroke-agent-authoring/references/testing.md +85 -0
- package/keystroke-cli-workspace/SKILL.md +93 -0
- package/keystroke-cli-workspace/evals/evals.json +23 -0
- package/keystroke-cli-workspace/references/command-map.md +50 -0
- package/keystroke-cli-workspace/references/credentials-and-connect.md +79 -0
- package/keystroke-cli-workspace/references/project-lifecycle.md +85 -0
- package/keystroke-credential-binding/SKILL.md +509 -0
- package/keystroke-credential-binding/evals/evals.json +29 -0
- package/keystroke-credential-binding/references/cli.md +85 -0
- package/keystroke-credential-binding/references/patterns.md +878 -0
- package/keystroke-credential-binding/references/source-map.md +69 -0
- package/keystroke-data-toolkit/SKILL.md +59 -0
- package/keystroke-data-toolkit/evals/evals.json +23 -0
- package/keystroke-data-toolkit/references/usage.md +79 -0
- package/keystroke-task-authoring/SKILL.md +124 -0
- package/keystroke-task-authoring/evals/evals.json +23 -0
- package/keystroke-task-authoring/references/patterns.md +132 -0
- package/keystroke-task-authoring/references/source-map.md +61 -0
- package/keystroke-trigger-authoring/SKILL.md +189 -0
- package/keystroke-trigger-authoring/evals/evals.json +29 -0
- package/keystroke-trigger-authoring/references/patterns.md +265 -0
- package/keystroke-trigger-authoring/references/source-map.md +128 -0
- package/keystroke-trigger-authoring/references/testing.md +148 -0
- package/keystroke-workflow-as-tool-debugging/SKILL.md +52 -0
- package/keystroke-workflow-as-tool-debugging/evals/evals.json +23 -0
- package/keystroke-workflow-as-tool-debugging/references/playbook.md +77 -0
- package/keystroke-workflow-authoring/SKILL.md +234 -0
- package/keystroke-workflow-authoring/evals/evals.json +29 -0
- package/keystroke-workflow-authoring/references/patterns.md +265 -0
- package/keystroke-workflow-authoring/references/prebuilt-integrations.md +811 -0
- package/keystroke-workflow-authoring/references/runtime-helpers.md +264 -0
- package/keystroke-workflow-authoring/references/source-map.md +108 -0
- package/keystroke-workflow-authoring/references/testing.md +108 -0
- 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
|