← Key Shape
API
Tokens

Run Key Shape from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page checks the key format (prefix, environment marker, entropy, checksum, scanner detectability), verifies the user's test keys in the browser - they are never part of the input - runs the storage, scoping, rotation, dashboard, ownership, governance and OWASP API2 checks, and ranks the storage and scoping rungs, then hands it all to the run as facts. The run writes the design or the runbook; the page then holds it to its own facts (keyreply.js): the ranked rungs, an environment prefix, 120+ bits, a checksum, an invented example key that follows the format, verifies and matches its pattern, the rotation numbers, the four self-service actions, a service-account tier; a rotation that creates before it revokes and waits for zero traffic, a leaked key revoked at once, and no key printed.

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"key-shape"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, platform, keys, facts) is missing. The page always sends all five of task, platform, keys, carry, facts; carry is an empty string for design.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the keys, then the bad day:

A missing or unknown task is answered as design. Both lanes send the same five fields; no pasted key is ever one of them. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the design's decisions into carry for the runbook lane.

The input

FieldRequiredWhat
taskyesdesign or runbook. If it is missing or neither, do design and set "task": "design".
platformyesthe platform's name and purpose, and the interview: greenfield or retrofit, who holds keys, CI or agent consumers, third-party apps, compliance, existing RBAC, teams, lost keys, timing; "not given" means not given.
keysyesthe key format as described (prefixes, body, checksum), storage, scoping, rotation, the dashboard, offboarding, audit, expiry and the OWASP answers, and how many of the user's test keys matched - never the keys.
carrysend it (empty for design)runbook task only: the design's decisions, when the design task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, and the rungs the page ranked. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/api-auth-key-management - the only standard the run applies:

House rules

The replies

design

Write the key design.

  1. summary - one sentence, at most 40 words: what makes a leaked key hurt today and the first fix - the most dangerous failure first (a key in a browser or app, plaintext storage, scopes failing open, a missing revoke).
  2. format - prefixes (one object per prefix: prefix ending in _, meaning; one names the environment), body_chars (a number), alphabet (base62, hex, base64url or base32), checksum (crc6 for CRC32 of the body as 6 base62 characters - always base62, whatever the body alphabet - or a named alternative), scanner_regex (a pattern matching every key of the format: a prefix, the body, then the 6 checksum characters).
  3. storage - rung (the facts' rung: hash, retrievable or workload), why (one sentence).
  4. scoping - rung (the facts' rung: split or fine), scopes (the default scopes, as short strings, restrictive by default), why (one sentence).
  5. rotation - operation (one sentence: the atomic roll), grace_routine_days (1 to 7), grace_compromise_days (0), max_active (2), why (one sentence).
  6. dashboard - actions (["sign up", "create", "rotate", "revoke"] at least), list_fields (what each key row shows), masking (one sentence).
  7. ownership - tiers (one object per tier: tier - individual, team or service, use), offboarding (the steps, as short strings).
  8. governance - controls (short strings), evidence (one sentence).
  9. oauth - one sentence: where OAuth or short-lived tokens take over from keys.
  10. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "design",
  "summary": "one sentence",
  "format": { "prefixes": [ { "prefix": "sk_live_", "meaning": "secret key, production" } ], "body_chars": 32, "alphabet": "base62", "checksum": "crc6", "scanner_regex": "\\b(?:sk_live_|sk_test_)[0-9A-Za-z]{38}\\b" },
  "storage": { "rung": "hash", "why": "one sentence" },
  "scoping": { "rung": "split", "scopes": [ "read" ], "why": "one sentence" },
  "rotation": { "operation": "one sentence", "grace_routine_days": 7, "grace_compromise_days": 0, "max_active": 2, "why": "one sentence" },
  "dashboard": { "actions": [ "sign up", "create", "rotate", "revoke" ], "list_fields": [ "label" ], "masking": "one sentence" },
  "ownership": { "tiers": [ { "tier": "service", "use": "one sentence" } ], "offboarding": [ "a step" ] },
  "governance": { "controls": [ "a control" ], "evidence": "one sentence" },
  "oauth": "one sentence",
  "caveats": [ ]
}

The page holds it to its own facts (check() in keyreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no real-looking key but the one invented example, and never a key the user pasted; prefixes of letters ending in _, one naming the environment; a known alphabet and at least 120 bits; a checksum; an example key that starts with a prefix, has the right length and alphabet, verifies its crc6 checksum and matches the scanner pattern, which compiles and is not too broad; the ranked storage and scoping rungs; restrictive default scopes; 1 to 7 days of routine grace, none on compromise, at most two active keys; sign up, create, rotate and revoke; label, created and last-used per key; a service-account tier; offboarding with revocation; governance controls (an audit log when compliance is in scope); and a word on OAuth (OAuth when third-party apps act for users).

runbook

Write the rotation and leak-response runbook.

  1. summary - one sentence, at most 40 words.
  2. rotation - the scheduled rotation, in order: objects with step, owner (a role), check (how you know the step is done). The roll creates the new key and schedules the old one's revocation in one step; the old key is deleted later, only once last-used shows zero traffic.
  3. leak - the response to a leaked or suspected key, in order: objects with step, owner, within (how soon, as a duration).
  4. offboarding - the departure steps, in order, as short strings.
  5. comms - one or two sentences: what affected customers are told and when.
  6. metrics - two to four strings: what to track afterwards.
  7. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "runbook",
  "summary": "one sentence",
  "rotation": [ { "step": "one sentence", "owner": "integration owner", "check": "one sentence" } ],
  "leak": [ { "step": "one sentence", "owner": "on-call security", "within": "15 minutes" } ],
  "offboarding": [ "a step" ],
  "comms": "one or two sentences",
  "metrics": [ "time from detection to revocation" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; no URL; no backticks; no real-looking key; at least four rotation steps that create the new key before revoking the old, wait for zero traffic or last-used before deleting, and give each step an owner and a check; at least four leak steps that revoke at once, review the logs, and give each step an owner and a deadline; offboarding that enumerates, revokes and rotates shared credentials; what customers are told; and a metric for the time from detection to revocation.

Worked examples

Three runs from the page's free samples (invented platforms; every example key is a visible fake): the design for a payments API in production, the runbook for the same API carrying that design, and the design for a platform whose callers are CI pipelines and AI agents. Each input is exactly what the page sends - with no pasted key in it; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

design - A payments API in production

The page read it as: a leaked key would hurt (12 fail, 6 warn, 2 pass); entropy 96 bits; storage by rank: hash; scoping by rank: split; failing checks: env, entropy, storage, scoping, failopen, rotation, selfserve, offboard, audit, browser, ratelimit, fallback.

Input (facts is 2262 characters; the first 1397 are shown):

{
 "task": "design",
 "platform": "Platform: Quillmint Payments\nWhat it does: Card payments for small online shops; merchants call it from their servers and, for in-app checkout, from their mobile apps.\nMode: retrofit\nWho holds keys: Individual developers\nCI or AI-agent consumers: no\nThird-party apps act on behalf of users: no\nCompliance: PCI-DSS (cardholder data)\nExisting RBAC: no\nSeveral teams with distinct permission needs: no\nConsumers routinely lose keys: no\nTiming: Must ship within weeks",
 "keys": "Prefixes:\n- key_ | the only key type\nBody: 24 hex characters (96 bits); checksum: None\nStorage: Plaintext; scoping: Every key can do everything; failing open without scopes: yes\nRotation: No rotation; routine grace: not given; active keys per credential: not given\nDashboard: sign up yes, create yes, rotate no, revoke yes, masked after creation no, last-used shown no\nOffboarding: Nothing happens; audit log: no; expiry: Keys never expire\nKeys in browsers or apps: yes; validation rate-limited: no; constant-time compare: unknown; hardcoded fallback: yes\nSample test keys checked in the page: 1 of 1 match (the keys themselves are never sent)",
 "carry": "",
 "facts": "The page's checks (12 fail, 6 warn, 2 pass, 2 n/a):\n- Format | prefix | pass | key_\n- Format | env | fail | no prefix names an environment - a test key and a live key look alike\n- Format | scanner | warn | generic prefixes (key_) - add a short vendor mark\n- Format | entropy | fail | 24 hex characters = 96 bits\n- Format | checksum | warn | None\n- Format | samples | pass | all 1 sample match\n- Storage and scoping | storage | fail | Plaintext\n- Storage and scoping | scoping | fail | Every key can do everything\n- Storage and scoping | failopen | fail | a key created without scopes can do everything\n- Rotation and dashboard | rotation | fail | No rotation\n- Rotation and dashboard | grace | na | not given\n- Rotation and dashboard | maxactive | na | not given\n- Rotation and dashboard | selfserve | fail | missing: rotate\n- Rotation and dashboard | mask | warn | shown in full after creation, no last-used\n- Ownership and governance | service | warn | only individual keys - production runs on someone's personal credential\n- Ownership and governance | offboard | fail | Nothing happens\n- Ownership and governance | audit | fail | no audit log - PCI-DSS (cardholder data) asks for the evidence\n- Ownership and governance | expiry | warn | Keys never expire\n- OWASP API2 failure modes | browser | fail | long-lived keys ship to browsers or apps - mint short-lived tokens server-side\n- OWASP API2\n\n[... 865 more characters cut from this display ...]"
}

Reply:

{
 "task": "design",
 "summary": "A leaked key today is plaintext, unscoped, never expires and also ships inside mobile apps, so one copy can move money forever; first take keys out of the apps and store only their SHA-256 hash.",
 "format": {
  "prefixes": [
   {
    "prefix": "cpsk_live_",
    "meaning": "Quillmint secret key, live mode - moves real card payments"
   },
   {
    "prefix": "cpsk_test_",
    "meaning": "Quillmint secret key, test mode - sandbox cards only, never real money"
   }
  ],
  "body_chars": 32,
  "alphabet": "base62",
  "checksum": "crc6",
  "scanner_regex": "\\bcpsk_(?:live|test)_[0-9A-Za-z]{38}\\b"
 },
 "storage": {
  "rung": "hash",
  "why": "Merchants do not routinely lose keys and the ship date is weeks away, so show each key once and keep only its SHA-256 hash - never the plaintext, so a database dump stops being a key dump."
 },
 "scoping": {
  "rung": "split",
  "scopes": [
   "charges:create",
   "charges:read"
  ],
  "why": "One key per merchant server per environment with coarse scopes is the rung the deadline allows; a key created without scopes gets only these two, and refunds:create or payouts:read must be granted explicitly."
 },
 "rotation": {
  "operation": "A single roll call creates the replacement key and schedules the old key's revocation at an expiry the merchant picks, so no rotation can leave the old key alive by accident.",
  "grace_routine_days": 7,
  "grace_compromise_days": 0,
  "max_active": 2,
  "why": "Seven days lets a merchant redeploy its servers on a normal release cycle, while a suspected compromise revokes at once as PCI-DSS 4.0 requires."
 },
 "dashboard": {
  "actions": [
   "sign up",
   "create",
   "rotate",
   "revoke",
   "set expiry",
   "view usage"
  ],
  "list_fields": [
   "label",
   "prefix and last 4 characters",
   "environment",
   "scopes",
   "created date and creator",
   "last used",
   "expiry",
   "status"
  ],
  "masking": "The full key is shown once at creation with a copy button, then only its prefix and last 4 characters; validation answers 401 for a bad key and 403 for a valid key missing the scope."
 },
 "ownership": {
  "tiers": [
   {
    "tier": "individual",
    "use": "Test-mode keys a developer creates to try the API; they cannot be created in live mode."
   },
   {
    "tier": "team",
    "use": "Live keys owned by the merchant account rather than a person, created and reviewed by the account owner."
   },
   {
    "tier": "service",
    "use": "Service-account keys for each merchant server that runs unattended, so production never runs on a departed developer's personal key."
   }
  ],
  "offboarding": [
   "List every key the leaver created or could see from the merchant account's key view",
   "Revoke their personal keys with zero grace",
   "Roll any shared live key they could see and move it to a service account",
   "Log each revocation and roll with who did it and when"
  ]
 },
 "governance": {
  "controls": [
   "Tamper-resistant audit log of every create, roll, revoke and failed validation, kept 12 months",
   "Rotation cadence set by a targeted risk analysis, with immediate rotation on suspected compromise",
   "Mandatory expiry on live keys with warnings before it lands",
   "Rate limits on validation and key creation, treated like a login endpoint",
   "Constant-time hash comparison and no hardcoded fallback key"
  ],
  "evidence": "The audit log, the written risk analysis behind the cadence and the expiry and revocation records are the evidence a PCI-DSS 4.0 assessor asks for under requirement 8.6.3, kept tamper-resistant for 12 months."
 },
 "oauth": "Mobile in-app checkout stops carrying any key: the merchant server mints a short-lived, single-payment token and the app uses only that, while static keys stay on servers.",
 "caveats": [
  "Every key already shipped in a mobile app must be treated as exposed and rolled once the app no longer needs it."
 ]
}

What step 5 prints for it: cpsk_live_ | cpsk_test_ | hash | split

The page's check: kept - nothing to flag

runbook - A payments API in production

The input carries the saved design's decisions (R.laneForm fills carry from the design reply), as the page's handoff does.

Input (facts is 2181 characters; the first 1397 are shown):

{
 "task": "runbook",
 "platform": "Platform: Quillmint Payments\nWhat it does: Card payments for small online shops; merchants call it from their servers and, for in-app checkout, from their mobile apps.\nMode: retrofit\nWho holds keys: Individual developers\nCI or AI-agent consumers: no\nThird-party apps act on behalf of users: no\nCompliance: PCI-DSS (cardholder data)\nExisting RBAC: no\nSeveral teams with distinct permission needs: no\nConsumers routinely lose keys: no\nTiming: Must ship within weeks",
 "keys": "Prefixes:\n- key_ | the only key type\nBody: 24 hex characters (96 bits); checksum: None\nStorage: Plaintext; scoping: Every key can do everything; failing open without scopes: yes\nRotation: No rotation; routine grace: not given; active keys per credential: not given\nDashboard: sign up yes, create yes, rotate no, revoke yes, masked after creation no, last-used shown no\nOffboarding: Nothing happens; audit log: no; expiry: Keys never expire\nKeys in browsers or apps: yes; validation rate-limited: no; constant-time compare: unknown; hardcoded fallback: yes\nSample test keys checked in the page: 1 of 1 match (the keys themselves are never sent)",
 "carry": "Prefixes: cpsk_live_, cpsk_test_\nStorage: hash\nScoping: split (charges:create, charges:read)\nRotation: A single roll call creates the replacement key and schedules the old key's revocation at an expiry the merchant picks, so no rotation can leave the old key alive by accident. Routine grace 7 days, compromise 0, at most 2 active.\nTiers: individual, team, service\nDashboard: sign up, create, rotate, revoke, set expiry, view usage",
 "facts": "The page's checks (12 fail, 6 warn, 2 pass, 2 n/a):\n- Format | prefix | pass | key_\n- Format | env | fail | no prefix names an environment - a test key and a live key look alike\n- Format | scanner | warn | generic prefixes (key_) - add a short vendor mark\n- Format | entropy | fail | 24 hex characters = 96 bits\n- Format | checksum | warn | None\n- Format | samples | pass | all 1 sample match\n- Storage and scoping | storage | fail | Plaintext\n- Storage and scoping | scoping | fail | Every key can do everything\n- Storage and scoping | failopen | fail | a key created without scopes can do everything\n- Rotation and dashboard | rotation | fail | No rotation\n- Rotation and dashboard | grace | na | not given\n- Rotation and dashboard | maxactive | na | not given\n- Rotation and dashboard | selfserve | fail | missing: rotate\n- Rotation and dashboard | mask | warn | shown in full after creation, no last-used\n- Ownership and governance | service | warn | only individual keys - production runs on someone's personal credential\n- Ownership and governance | offboard | fail | Nothing happens\n- Ownership and governance | audit | fail | no audit log - PCI-DSS (cardholder data) asks for the evidence\n- Ownership and governance | expiry | warn | Keys never expire\n- OWASP API2 failure modes | browser | fail | long-lived keys ship to browsers or apps - mint short-lived tokens server-side\n- OWASP API2\n\n[... 784 more characters cut from this display ...]"
}

Reply:

{
 "task": "runbook",
 "summary": "Roll each live key on the cadence the PCI-DSS risk analysis sets, retire the old one only after its traffic reaches zero, and revoke a leaked key within minutes with zero grace before reviewing every charge it made.",
 "rotation": [
  {
   "step": "List the live keys that reach the rotation date set by the targeted risk analysis, with each key's owner and service account.",
   "owner": "security lead",
   "check": "Every key on the list has a named owner and a rotation ticket."
  },
  {
   "step": "Run the roll call for each key: it creates the replacement cpsk_live_ key and schedules the old key's revocation 7 days out.",
   "owner": "merchant integration owner",
   "check": "The dashboard shows exactly two active keys for the credential, the old one with its scheduled date."
  },
  {
   "step": "Put the new key in the merchant server's secret store and redeploy every server that calls the API.",
   "owner": "merchant integration owner",
   "check": "Last-used on the new key advances and charges succeed with it."
  },
  {
   "step": "Watch the old key's last-used and usage until it shows zero traffic for a full day; extend the schedule if anything still calls it.",
   "owner": "merchant integration owner",
   "check": "Last-used on the old key is more than 24 hours old and its request count is zero."
  },
  {
   "step": "Let the scheduled revocation land, then delete the old key's record only once it has stayed at zero traffic.",
   "owner": "platform engineer",
   "check": "A request with the old key returns 401 and no merchant error rate rises."
  },
  {
   "step": "Write the rotation to the audit log with who rolled the key, when, and the risk-analysis reference.",
   "owner": "security lead",
   "check": "The audit log entry exists and is in the 12-month tamper-resistant store."
  }
 ],
 "leak": [
  {
   "step": "Confirm the report: match the prefix and checksum, then find the key by its SHA-256 hash and identify its merchant and owner.",
   "owner": "on-call security",
   "within": "15 minutes"
  },
  {
   "step": "Revoke the key immediately with zero grace and roll a replacement for the merchant, without waiting for the merchant to redeploy.",
   "owner": "on-call security",
   "within": "15 minutes"
  },
  {
   "step": "Review the audit and request logs for everything the key did since it could have leaked: charges, refunds, source addresses and failed validations.",
   "owner": "security analyst",
   "within": "4 hours"
  },
  {
   "step": "Find and remove the exposed copy - repository, log, or mobile app build - and if it shipped in an app, move that app to server-minted short-lived tokens.",
   "owner": "merchant integration owner",
   "within": "24 hours"
  },
  {
   "step": "Decide with compliance whether cardholder data was reachable and start the PCI-DSS incident process if so.",
   "owner": "compliance lead",
   "within": "24 hours"
  },
  {
   "step": "Hold a review of how the key escaped and record the fix and the timings in the audit log.",
   "owner": "security lead",
   "within": "5 business days"
  }
 ],
 "offboarding": [
  "List every key the leaver created or could see, from the account's key view and the audit log",
  "Revoke their personal keys with zero grace",
  "Roll every shared live key they could see and move it onto a service account",
  "Reassign any service account they owned to a current owner",
  "Log each revocation and roll with who did it and when"
 ],
 "comms": "The affected merchant is told as soon as the key is revoked: which key (by label and last 4 characters), that it no longer works, the replacement to deploy, and once the log review ends, any charges or refunds made with it.",
 "metrics": [
  "Time from detection to revocation",
  "Live keys still running on a personal key instead of a service account",
  "Keys past the rotation date the risk analysis set",
  "Old keys still carrying traffic when their scheduled revocation arrives"
 ],
 "caveats": []
}

What step 5 prints for it: 15 minutes: Confirm the report: match the prefix and | 15 minutes: Revoke the key immediately with zero gra | 4 hours: Review the audit and request logs for ev | 24 hours: Find and remove the exposed copy - repos | 24 hours: Decide with compliance whether cardholde | 5 business days: Hold a review of how the key escaped and

The page's check: kept - nothing to flag

design - A platform for CI and AI agents

The page read it as: a leaked key would hurt (1 fail, 1 warn, 20 pass); entropy 190.5 bits; storage by rank: workload; scoping by rank: fine; failing checks: samples.

Input (facts is 2085 characters; the first 1399 are shown):

{
 "task": "design",
 "platform": "Platform: Nebbleworks\nWhat it does: A build and deploy platform; most keys are used by CI pipelines and AI coding agents, the rest by developers trying things.\nMode: retrofit\nWho holds keys: A mix of people, teams and machines\nCI or AI-agent consumers: yes\nThird-party apps act on behalf of users: no\nCompliance: SOC 2 (enterprise buyers)\nExisting RBAC: yes\nSeveral teams with distinct permission needs: no\nConsumers routinely lose keys: no\nTiming: A platform asset for years",
 "keys": "Prefixes:\n- nbl_live_ | secret key, production\n- nbl_test_ | secret key, test mode\nBody: 32 base62 characters (190.5 bits); checksum: CRC32 of the body, 6 base62 characters (this page's scheme)\nStorage: SHA-256 hash only; scoping: Per-environment keys with coarse scopes; failing open without scopes: no\nRotation: One atomic roll with an overlap; routine grace: 7 days; active keys per credential: 2\nDashboard: sign up yes, create yes, rotate yes, revoke yes, masked after creation yes, last-used shown yes\nOffboarding: A runbook run on every departure; audit log: yes; expiry: Optional expiry\nKeys in browsers or apps: no; validation rate-limited: yes; constant-time compare: yes; hardcoded fallback: no\nSample test keys checked in the page: 1 of 2 match (the keys themselves are never sent)",
 "carry": "",
 "facts": "The page's checks (1 fail, 1 warn, 20 pass, 0 n/a):\n- Format | prefix | pass | nbl_live_, nbl_test_\n- Format | env | pass | nbl_live_, nbl_test_\n- Format | scanner | pass | distinctive\n- Format | entropy | pass | 32 base62 characters = 190.5 bits\n- Format | checksum | pass | CRC32 of the body, 6 base62 characters (this page's scheme)\n- Format | samples | fail | 1 of 2 samples do not match (see the sample table)\n- Storage and scoping | storage | pass | SHA-256 hash only\n- Storage and scoping | scoping | pass | Per-environment keys with coarse scopes\n- Storage and scoping | failopen | pass | no (as stated)\n- Rotation and dashboard | rotation | pass | One atomic roll with an overlap\n- Rotation and dashboard | grace | pass | 7 days\n- Rotation and dashboard | maxactive | pass | 2 active at once\n- Rotation and dashboard | selfserve | pass | all four\n- Rotation and dashboard | mask | pass | masked, last-used shown\n- Ownership and governance | service | pass | A mix of people, teams and machines\n- Ownership and governance | offboard | pass | A runbook run on every departure\n- Ownership and governance | audit | pass | logged\n- Ownership and governance | expiry | warn | Optional expiry\n- OWASP API2 failure modes | browser | pass | no (as stated)\n- OWASP API2 failure modes | ratelimit | pass | yes\n- OWASP API2 failure modes | consttime | pass | yes\n- OWASP API2 failure modes | fallback |\n\n[... 686 more characters cut from this display ...]"
}

Reply:

{
 "task": "design",
 "summary": "Most keys sit unattended in CI pipelines and AI agents with optional expiry, so a leaked one stays valid indefinitely; first move those callers to workload identity and make expiry mandatory on every key left.",
 "format": {
  "prefixes": [
   {
    "prefix": "nbl_live_",
    "meaning": "Nebbleworks secret key, production - can build and deploy real projects"
   },
   {
    "prefix": "nbl_test_",
    "meaning": "Nebbleworks secret key, test mode - sandbox projects only"
   }
  ],
  "body_chars": 32,
  "alphabet": "base62",
  "checksum": "crc6",
  "scanner_regex": "\\bnbl_(?:live|test)_[0-9A-Za-z]{38}\\b"
 },
 "storage": {
  "rung": "workload",
  "why": "CI systems and AI agents exchange their own signed workload identity for a short-lived token, so nothing long-lived is stored for them, and the remaining developer keys stay hash-only as today."
 },
 "scoping": {
  "rung": "fine",
  "scopes": [
   "builds:read on one project",
   "builds:trigger on one project",
   "deploys:write on one named environment"
  ],
  "why": "SOC 2 buyers expect least privilege they can review, so every key and token names its project and environment and must carry an expiry of 90 days at most, and a key created without scopes can only read builds."
 },
 "rotation": {
  "operation": "One roll call creates the new key and schedules the old key's expiry at a time the caller picks, keeping at most two keys alive for that credential.",
  "grace_routine_days": 7,
  "grace_compromise_days": 0,
  "max_active": 2,
  "why": "Seven days covers pipelines and agent fleets that redeploy slowly and queued jobs still holding the old key, while compromise and departures revoke at once."
 },
 "dashboard": {
  "actions": [
   "sign up",
   "create",
   "rotate",
   "revoke",
   "set expiry",
   "view usage"
  ],
  "list_fields": [
   "label",
   "prefix and last 4 characters",
   "owner or service account",
   "project and environment scopes",
   "created date",
   "last used",
   "expiry date",
   "status"
  ],
  "masking": "The key appears once at creation and afterwards only as its prefix and last 4 characters; last-used is updated asynchronously and usage charts sit apart from it."
 },
 "ownership": {
  "tiers": [
   {
    "tier": "individual",
    "use": "Short-lived test-mode keys for developers trying things, never allowed on a production deploy."
   },
   {
    "tier": "team",
    "use": "Team keys for shared tooling, created with an organization admin's approval and listed in the quarterly access review."
   },
   {
    "tier": "service",
    "use": "Service accounts for every pipeline and agent fleet, authenticated by workload identity where possible and by a scoped, expiring key where not."
   }
  ],
  "offboarding": [
   "List every key and token the leaver created from the admin view",
   "Revoke their personal keys with zero grace",
   "Roll any team or service-account key they could see",
   "Reassign service accounts they owned to a current owner",
   "Log each step for the SOC 2 evidence trail"
  ]
 },
 "governance": {
  "controls": [
   "Written key lifecycle policy: creation, scoping, expiry, rotation, revocation",
   "Mandatory expiry with warnings 14 and 3 days before",
   "Audit log of every create, roll, revoke and failed validation",
   "Quarterly access review of team and service-account keys",
   "Admin approval for team keys and live-mode scopes"
  ],
  "evidence": "Key-event logs, access-review sign-offs and approval records show SOC 2 auditors the policy operating across the whole observation window, not only on paper."
 },
 "oauth": "CI pipelines and AI coding agents stop holding static keys and trade workload identity for tokens that last minutes; static keys remain for developers experimenting in test mode and for the few callers that cannot federate.",
 "caveats": [
  "One of the two test keys pasted into the page does not match the format - find which generator produced it before trusting it."
 ]
}

What step 5 prints for it: nbl_live_ | nbl_test_ | workload | fine

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./keykit.js"); require("./keyreply.js")), then:

const K = KeyKit, R = KeyReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", prefixes: "acme_live_ | secret, production\nacme_test_ | secret, test",
  bodylen: "32", alphabet: "base62", checksum: "crc6", storage: "sha256", scoping: "coarse", rotation: "roll" }));
// f.rows (every check: area, id, label, status, evidence), f.bits, f.regex (the scanner pattern), f.store, f.scope
// K.generate(f, "acme_test_", n => crypto.getRandomValues(new Uint8Array(n))) - a fresh test key; K.readSample(f, key) - check one
const b = K.buildInput(f, "design");                         // or "runbook" (R.laneForm(form, "runbook", design) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run - it never carries a key
// then: const reply = R.normalize(R.parseReply(job.output.output), "design");
// R.check(reply, b)                                         // {status: kept or flagged, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://key-shape.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"key-shape"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "design", "platform": "Platform: Quillmint Payments\nMode: retrofit\n...", "keys": "Prefixes:\n- key_ | the only key type\n...", "carry": "", "facts": "The page's checks (12 fail, 6 warn, ...):\n- Format | env | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses key-shape:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A design reply carries summary, format, storage, scoping, rotation, dashboard, ownership, governance, oauth and caveats; a runbook reply carries summary, rotation, leak, offboarding, comms, metrics and caveats. The code below prints the gist of either: the prefixes and the two rungs, or each leak-response step with its deadline.

KEY="key-shape:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "runbook" then (.leak[] | "\(.within): \(.step)") 
       else "\(.format.prefixes | map(.prefix) | join(", ")) - \(.storage.rung), \(.scoping.rung)" end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: key-shape:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json