connection-profile
Connection profile schema design and implementation. Use when designing or implementing connectionProfile.yml or connectionState.yml files.
SKILL.md.claude/skills/connection-profile/SKILL.md
GitHubConnection Profile & State Design Rules
Rules for designing connectionProfile.yml and connectionState.yml schemas
🚨 CRITICAL RULES
Rule 1: Always Extend Core Profiles
NEVER create custom profiles from scratch - ALWAYS extend core profiles
# ❌ WRONG - Custom profile
type: object
properties:
apiKey:
type: string
# ✅ CORRECT - Extend core profile
$ref: './node_modules/@zerobias-org/types-core/schema/tokenProfile.yml'
Rule 2: connectionState MUST Extend baseConnectionState
🚨 MANDATORY: connectionState.yml MUST extend baseConnectionState.yml for expiresIn
# ✅ CORRECT - Extends base with expiresIn
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/baseConnectionState.yml'
- type: object
properties:
accessToken:
type: string
# ❌ WRONG - Missing baseConnectionState
type: object
properties:
accessToken:
type: string
# Missing expiresIn!
WHY: Server uses expiresIn to schedule automatic token refresh cronjobs
Rule 3: expiresIn Must Be in Seconds
# ✅ CORRECT
expiresIn:
type: integer
description: Token expiry time in SECONDS from now
# ❌ WRONG
expiresAt:
type: string # Don't use expiresAt, convert to expiresIn
Calculation: expiresIn = Math.floor((expiresAt - now) / 1000)
Core Profile Selection
Available Core Profiles
| Profile | When to Use | Contains |
|---|---|---|
| tokenProfile.yml | API Key, Bearer Token, PAT | token |
| oauthClientProfile.yml | OAuth2 Client Credentials | clientId, clientSecret |
| oauthTokenProfile.yml | OAuth2 with Refresh | clientId, clientSecret |
| basicConnection.yml | Username/Password, Email/Password | username, password |
Path: ./node_modules/@zerobias-org/types-core/schema/{profileName}.yml
Selection Decision Tree
@credential-manager provides authMethodType:
IF authMethodType == "bearer-token" OR "api-key"
→ EXTEND tokenProfile.yml
IF authMethodType == "oauth2-client-credentials"
→ EXTEND oauthClientProfile.yml
IF authMethodType == "oauth2-authorization-code"
→ EXTEND oauthTokenProfile.yml
IF authMethodType == "basic-auth"
→ EXTEND basicConnection.yml
Available Core States
| State | When to Use | Contains |
|---|---|---|
| tokenConnectionState.yml | Simple token auth | accessToken + expiresIn |
| oauthTokenState.yml | OAuth with refresh | accessToken + refreshToken + expiresIn |
State Selection
IF requiresRefresh == false
→ EXTEND tokenConnectionState.yml
IF requiresRefresh == true
→ EXTEND oauthTokenState.yml
Common Patterns
Pattern 1: Simple API Token
# connectionProfile.yml
$ref: './node_modules/@zerobias-org/types-core/schema/tokenProfile.yml'
# connectionState.yml
$ref: './node_modules/@zerobias-org/types-core/schema/tokenConnectionState.yml'
Pattern 2: OAuth Client Credentials
# connectionProfile.yml
$ref: './node_modules/@zerobias-org/types-core/schema/oauthClientProfile.yml'
# connectionState.yml (no refresh)
$ref: './node_modules/@zerobias-org/types-core/schema/tokenConnectionState.yml'
Pattern 3: OAuth with Refresh Token
# connectionProfile.yml
$ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenProfile.yml'
# connectionState.yml
$ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenState.yml'
🚨 CRITICAL: OAuth Click-to-Connect — 3 layers (only 1 is insider)
Choosing an OAuth authorization_code flow (the Hub "Connect" button) spans three layers, and
two of the three are contributor-self-serve content-as-code:
- Module — connectionProfile/state +
connect()/refresh()(you author). - Provider wiring — the
oauthartifact (index.yml {id,url}) + thex-oauth-providerslink (you author + load as content — even to your own org first). - External app + secret — register the OAuth app and put client_id/secret in AWS Secrets Manager (the only genuinely-insider step; secrets can't be content).
If you ship layers 1–2 but no one does layer 3, the button stays dark — so when OAuth authorization-code is chosen, open a task for layer 3.
The module half — YOU author this (fully self-serve)
Copy a real shipped OAuth module rather than authoring from scratch. Verified
reference implementations in @auditlogic/module-* (all use x-oauth-providers + refresh). Note
the $ref scope: current modules use @zerobias-org/types-core; older ones (e.g. slack) use
@auditmation/types-core — same schemas, use @zerobias-org/types-core for new work:
| Module | Provider | Good example of |
|---|---|---|
microsoft/azure/msgraph |
microsoft.oauth |
Entra/Azure AD — reuse microsoft.oauth for any Entra-backed product; dual-mode (OAuth or manual clientId/secret/directoryId) |
atlassian/cloud/jira |
atlassian.oauth |
explicit connectionMode: [manual, oauth] enum + oauth basePath rewrite |
slack/slack |
slack.oauth |
minimal OAuth-only profile; connect() hard-requires oauthConnectionDetails |
github/github |
github.oauth |
OAuth + PAT fallback |
connectionProfile.yml — extend BOTH oauthTokenProfile AND oauthTokenState, add the
provider hook, and use x-ui-* to control the connect form (this is what every real module does):
# connectionProfile.yml — OAuth authorization-code (mirrors msgraph/jira/slack)
type: object
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenProfile.yml'
- $ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenState.yml'
- type: object
x-oauth-providers:
- <vendor>.oauth # e.g. microsoft.oauth for Entra/Azure AD-backed products
x-ui-required: # fields the connect form REQUIRES (manual-entry / OAuth-app creds)
- clientId
- clientSecret
x-ui-hidden: # HIDE the token-state fields the OAuth flow fills in for you
- tokenType
- accessToken
- refreshToken
- expiresIn
- scope
- url
properties:
clientId:
type: string
clientSecret:
type: string
format: password
⚠️
x-ui-*= which fields the connect FORM shows. Rule of thumb:
x-ui-hidden: the OAuth-flow fields the user should never type — always hidetokenType,refreshToken,expiresIn,scope,url.x-ui-required: the fields that ARE the manual connect method. This is a real design choice, not fixed:msgraphrequires the app creds (clientId/clientSecret/directoryId) and HIDESaccessToken;slackinstead REQUIRESaccessToken(it supports pasting a bot token) and hides the rest. Pick per module: hideaccessTokenfor pure click-to-connect; require it only if you also support manual token paste. (The "Build-Time Resolution → The Pattern" example lower in this file shows the require-accessTokenvariant — that's the slack-style choice, not a contradiction.)
connectionState.yml — extend oauthTokenState.yml (accessToken + refreshToken +
expiresIn in SECONDS + scope). It MUST resolve to baseConnectionState (which oauthTokenState
already does) so the server schedules the refresh cronjob.
src/<Class>Impl.ts — connect() / refresh(). The OAuth app creds arrive at runtime as
a typed 2nd argument, OauthConnectionDetails (from @zerobias-org/types-core-js) — never
hardcode or persist them. Real signature (from msgraph/github):
import { OauthConnectionDetails } from '@zerobias-org/types-core-js';
async connect(
connectionProfile: ConnectionProfile,
oauthConnectionDetails?: OauthConnectionDetails,
): Promise<BaseConnectionState> { /* exchange code/refresh → accessToken */ }
async refresh(
connectionProfile: ConnectionProfile,
connectionState: BaseConnectionState,
oauthConnectionDetails?: OauthConnectionDetails,
): Promise<BaseConnectionState> {
// canonical: delegate to connect() with refresh=true
return this.client.connect(connectionProfile, oauthConnectionDetails, true);
}
Gotchas the real modules encode:
- Accept both casings. The node→module boundary may deliver
clientIdORclient_id. msgraph readsoauth?.clientId ?? oauth?.client_id— a forced refresh silently lost creds until it handled both. Do the same forclientSecret/client_secret. - Dual-mode fallback. OAuth is not strictly either/or with token auth. msgraph/jira accept
the OAuth popup creds or fall back to
connectionProfile.clientId/clientSecretfor a manual client-credentials connection. ThrowInvalidCredentialsError(or a specific message naming the missing field) only when neither path has what it needs.
🚨 What the module does — and does NOT — do at runtime
The platform's Global App performs the authorization-code exchange (the popup, code,
redirect_uri) and hands your module an already-minted accessToken + refreshToken in the
connection state. So do NOT implement a code→token exchange in the module — that's a common
wasted effort. Your connect()/refresh() only:
connect()(normal OAuth): just USE / validate theaccessTokenyou were handed.refresh(): re-mint the token yourself —POSTto the token endpoint withgrant_type: refresh_token,refresh_tokenfrom the profile/state, andclient_id/client_secretfromOauthConnectionDetails. (Only the manual dual-mode fallback usesgrant_type: client_credentials.)
🚨 Where each URL / scope lives (non-obvious — get this right)
| Thing | Lives in | Example (msgraph) |
|---|---|---|
| Authorize URL | the oauth provider index.yml (url) |
login.microsoftonline.com/…/oauth2/v2.0/authorize |
| Token URL | hardcoded constant in your client — NOT the provider | const BASE_TOKEN_URI + tenant + '/oauth2/v2.0/token' |
| Scopes | hardcoded constant in your client (api.yml securityScheme only documents them) | const MS_GRAPH_SCOPE |
accessToken / refreshToken |
read them off connectionProfile at runtime (see note) |
this.connectionProfile.refreshToken |
connect()has noconnectionStateargument — the tokens arrive onconnectionProfile. Because the profileallOf-includesoauthTokenState,accessToken/refreshToken/expiresInare fields on the profile object the Hub hands you. Real modules readthis.connectionProfile.accessToken/.refreshToken— do NOT look for a separate state param. (refresh()does receive aconnectionStatearg for metadata, but the refresh token still comes from the profile.)
The provider artifact carries only the authorize URL + a UUID. The token endpoint and scopes
are yours to hardcode in connect()/refresh() (they're universal per-provider constants — don't
put them in the profile or provider).
🚨 Testing an OAuth module locally (there's no popup in a test run)
Every connectionProfile field is supplied as a zbb secret whose name matches the field name;
describeModule<T> injects them into the ConnectionProfile passed to connect() (non-secret test
params go via zbb env set → test/e2e/constants.ts). The catch: there's no OAuth popup in
testDirect/testDocker, so something has to supply a token. Two real paths:
- Dual-mode / app-creds (easiest — what makes
msgraphtestable): if the module also supports a manualclient_credentialspath, storeclientId/clientSecret(+directoryId/tenant) as secrets;connect()mints its ownaccessToken— no popup, no pre-minted token needed.zbb secret create clientId --module @zerobias-org/module-<v>-<p> --slot local zbb secret create clientSecret --module @zerobias-org/module-<v>-<p> --slot local - Pure click-to-connect only: there's no way to mint a token locally — obtain a real
accessToken/refreshTokenonce, out-of-band (Postman/curl against the provider) and store them as secrets soconnect()/refresh()can run. Teststhis.skip()gracefully when the values are absent, so CI stays green without them.
Reference e2e wiring: msgraph, jira (test/e2e/ + constants.ts).
The <vendor>.oauth code refers to an OAuthProvider resource in the zerobias-org/oauth
repo. Reuse an existing provider when one fits (e.g. microsoft /
login.microsoftonline.com/organizations/oauth2/v2.0/authorize already exists — reuse it for
any Entra/Azure AD-backed product) rather than inventing a new one.
The provider wiring — content-as-code YOU author + load (NOT insider)
oauth is a loadable artifact type (see .claude/docs/dataloader-artifact-map.md), just
like vendor/product/module. The provider lives in the zerobias-org/oauth repo as
@zerobias-org/oauth-<vendor> and its only manifest is index.yml:
# zerobias-org/oauth · package/<vendor>/index.yml (nest package/<vendor>/<suite>/ for a suite)
id: 9d975c18-45d4-4974-abcb-90aea1795c12 # stable UUID (repo-wide unique)
url: https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize # public authorize URL
No client_id, no secret — just the public authorize URL + a UUID. The package's package.json
zerobias block deps on the vendor/suite/product package, which resolves the provider to a VSP
by package-code convention. The module references it via x-oauth-providers: [<vendor>.oauth];
the two meet in OAuthProvider at dataload.
So the provider and the module→provider link are authored and loaded exactly like any other
content — you can even load them into your OWN org first (org-private) before PRing to the shared
catalog. Reuse an existing provider when one fits — already present:
microsoft, github, atlassian, slack, zoho, google. (e.g. reuse oauth-microsoft for
any Entra/Azure AD product; only add a new oauth-<vendor> package if none matches.)
🚨 ALWAYS detect first — is the provider already in the repo?
Whenever a module needs an authorization-code provider, check the oauth repo live — do NOT
trust the static list above (it drifts as providers are added). Match your
vendor (and suite/product, if the provider is scoped):
gh api repos/zerobias-org/oauth/contents/package --jq '.[].name' # top-level vendors
gh api repos/zerobias-org/oauth/git/trees/main:package?recursive=1 \
--jq '.tree[] | select(.path|endswith("index.yml")) | .path' # every provider (incl. nested suite/product)
Decision:
- Provider exists → reference it via
x-oauth-providers: [<code>]and STOP. Nothing touches the oauth repo. - Provider MISSING → you must add it — author the package with the recipe below and open a
separate content PR to
zerobias-org/oauth(contributor content, NOT the insider task). Flag this as an extra deliverable so it isn't forgotten; the module can't advertise OAuth until the provider is loaded.
Recipe: add a missing provider (a zerobias-org/oauth content PR)
Prerequisite: the
dependenciesbelow reference@zerobias-org/vendor-<vendor>(or the suite/product package). That catalog content must already exist — if the vendor/product isn't in the catalog yet, create it FIRST via/create-product, then add the oauth provider. A provider whose vendor dep doesn't resolve won't load.
No scaffold script and no gradle gate — it's a 2-file Lerna package. Copy an existing
package/<vendor>/ and change 4 things (generate the UUID with uuidgen):
# package/<vendor>/index.yml
id: <fresh v4 UUID> # `uuidgen` — repo-wide unique, IMMUTABLE once published
url: https://<provider>/oauth2/authorize # the AUTHORIZE endpoint (not the token endpoint)
// package/<vendor>/package.json (only the load-bearing keys shown)
{
"name": "@zerobias-org/oauth-<vendor>",
"files": ["index.yml"],
"scripts": { "nx:publish": "../../scripts/publish.sh" },
"zerobias": {
"package": "<vendor>.oauth", // ← the code x-oauth-providers resolves against
"import-artifact": "oauth", // ← tells the dataloader the artifact type
"dataloader-version": "<current>" // copy from a sibling package's package.json
},
"dependencies": { "@zerobias-org/vendor-<vendor>": "latest" } // ← binds to the VSP (must exist)
}
- VSP scope = whatever package you depend on: vendor-level →
@zerobias-org/vendor-<vendor>+ code<vendor>.oauth; suite/product-scoped → depend on that package + code<vendor>.<suite>.<product>.oauth, nested atpackage/<vendor>/<suite>/…. - Publishes via Lerna to GitHub npm, then loads via the dataloader (
import-artifact: oauth). - PR base is
main— thezerobias-org/oauthrepo has nodevbranch.
The genuinely-insider half — external app + secret (a contributor CANNOT self-serve)
Only two things are NOT content-as-code, because they involve an external OAuth app and a secret. When OAuth authorization-code is chosen, tell the user to open a task for JUST these:
Title: Register <vendor> OAuth app + Secrets Manager entry for click-to-connect
The <module-package> connector is OAuth-capable and its oauth provider artifact + connectionProfile
link are authored/loaded as content. Two insider steps remain before the "Connect" button works:
1. Register (or confirm) the <vendor> OAuth app with the provider → obtain client_id +
client_secret; allow-list ZeroBias's shared Global App redirect URI; grant scopes/admin-consent.
(For a reused provider like Microsoft, the shared app may already exist.)
2. Store client_id/client_secret in AWS Secrets Manager (dev + prod), keyed to the
<vendor>.oauth OAuthProvider. Secrets cannot live in loadable content — this is the hard step.
Outcome: OAuth-enabled <vendor> connections show the click-to-Connect button in Hub.
If the provider package doesn't exist yet, authoring it (
@zerobias-org/oauth-<vendor>,index.yml {id,url}) is your job, not the insider's — it's a normal content PR tozerobias-org/oauth. The insider task is only the external app + the secret.
OAuth client-credentials (Pattern 2) is different: it needs no click-to-connect popup and no provider link — the user supplies clientId/clientSecret directly. No insider task, fully contributor-self-serve. Extend
oauthClientProfile(notoauthTokenProfile), and do NOT addx-oauth-providers. Example (M2M / service account): many products expose a service account (a Client ID + Client Secret from their admin console) exchanged viaclient_credentialsat the product's own token endpoint — often an Auth0/Cognito-backed URL distinct from the vendor's user-SSO IdP. Don't assume the token IdP from who owns the product; verify the actual token endpoint. Such a module is Pattern 2 end-to-end — no insider task. The registration task is only for the authorization_code ("Connect" button) flow.
Pattern 4: Custom Fields (Extend Core)
# connectionProfile.yml - Add organizationId
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/tokenProfile.yml'
- type: object
required: [organizationId]
properties:
organizationId:
type: string
description: Organization identifier for API access
# connectionState.yml - Add extra state
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/tokenConnectionState.yml'
- type: object
properties:
organizationName:
type: string
description: Cached organization name
Connection Profile Scope
What Belongs in connectionProfile
ONLY connection parameters - minimal set needed to CONNECT
# ✅ GOOD - Connection parameters
- token / apiKey / clientId / clientSecret
- baseUrl (if API has multiple environments)
- organizationId (if required to authenticate)
# ❌ BAD - Operation parameters
- limit (pagination parameter)
- projectId (operation scope, not connection scope)
- fields (query parameter)
Rule: If you can connect WITHOUT it, it's an operation parameter (not profile)
Check for Additional Optional Parameters
Always review API docs for environment-specific optional parameters:
# Example: Service has optional region parameter
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/tokenProfile.yml'
- type: object
properties:
region:
type: string
description: Optional region for multi-region deployments
enum: [us-east-1, eu-west-1, ap-southeast-1]
# region is optional, but include it if API docs mention it
Don't omit parameters that some environments might need!
🚨 CRITICAL: Check Parent Schema First
Before adding ANY property, check what the parent schema provides:
# Check what tokenProfile.yml provides
cat node_modules/@zerobias-org/types-core/schema/tokenProfile.yml
# Check what basicConnection.yml provides
cat node_modules/@zerobias-org/types-core/schema/basicConnection.yml
# Check what baseConnectionState.yml provides
cat node_modules/@zerobias-org/types-core/schema/baseConnectionState.yml
Avoid Semantic Duplication
# ❌ WRONG - basicConnection already has uri
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/basicConnection.yml'
- type: object
properties:
url: # Duplicate! basicConnection has uri
type: string
# ✅ CORRECT - Use parent's uri, or extend if truly different
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/basicConnection.yml'
# uri is already provided by basicConnection
Semantic duplicates to avoid:
- url / uri / baseUrl
- token / accessToken
- username / user / userName
- password / pass / pwd
🚨 CRITICAL: ALWAYS Check Product Documentation
Before finalizing connectionProfile, check product docs for ALL auth parameters:
# Example: Missing mfaCode from product docs
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/basicConnection.yml'
- type: object
properties:
mfaCode: # Found in product docs!
type: string
description: Multi-factor authentication code (optional)
Don't assume - verify:
- Read product documentation authentication section
- Look for optional parameters (region, environment, mfaCode, etc.)
- Include ALL mentioned parameters (even if optional)
- Don't omit parameters some environments might need
🚨 CRITICAL: Connection Scope MUST NOT Limit Operations
NEVER add organization/project/workspace IDs to connection:
# ❌ WRONG - Limits connection to single organization
connectionState:
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/baseConnectionState.yml'
- type: object
properties:
accessToken:
type: string
organizationId: # NO! Limits scope
type: string
# ✅ CORRECT - Connection works across all organizations
connectionState:
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/baseConnectionState.yml'
- type: object
properties:
accessToken:
type: string
# organizationId is operation parameter, NOT connection parameter
WHY: Connection should be reusable across all scopes. Use operation parameters for scope.
Scope identifiers belong in operation parameters, NOT connection:
- ❌ organizationId in connection
- ❌ workspaceId in connection
- ❌ projectId in connection
- ❌ teamId in connection
- ✅ These are operation parameters (use in API paths)
🚨 CRITICAL: Only Add Used Fields (for connectionState)
For connectionState - don't add fields "just in case" - only add what's actually USED:
# ❌ WRONG - identityId not used anywhere
connectionState:
properties:
accessToken: string
identityId: string # Where is this used? Nowhere!
# ✅ CORRECT - Only fields that are actually used
connectionState:
properties:
accessToken: string
# If connect() doesn't return identityId, don't add it
Ask:
- Does connect() method return this field?
- Do operations use this field?
- Is it needed for refresh/reconnect? If NO to all → don't add it
For connectionProfile - include environment-optional fields:
# ✅ CORRECT - Include optional fields that some environments need
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/tokenProfile.yml'
- type: object
properties:
region:
type: string
description: Optional region (needed in some deployments)
mfaCode:
type: string
description: Multi-factor authentication code (optional)
# These aren't required, but include them if product docs mention them
connectionProfile vs connectionState:
- connectionProfile: Include optional fields that might be needed in some environments (region, mfaCode, etc.)
- connectionState: Only fields actually used by connect() or operations
Validation Checklist
Before finalizing connection schemas:
- Checked parent schema - no semantic duplicates (url/uri, token/accessToken)
- Checked product docs thoroughly - all auth parameters included (mfaCode, region, etc.)
- connectionProfile extends a core profile (NOT custom)
- connectionState extends baseConnectionState (includes expiresIn)
- expiresIn is in seconds (integer), not expiresAt timestamp
- If refresh capability: state extends oauthTokenState
- If simple token: state extends tokenConnectionState
- Custom fields use allOf to extend core
- All required fields marked as required
- Checked API docs for optional connection parameters (region, environment, etc.)
- Profile contains ONLY connection parameters (not operation parameters)
- NO scope limitation - no organizationId/projectId/workspaceId in connection
- Only used fields - every state field is actually used by connect() or operations
- Minimal set needed to connect (don't add operation scope parameters)
🚨 CRITICAL: Build-Time Schema Resolution
ALWAYS use $ref in committed files - resolve at build time
Why Build-Time Resolution?
- Clean source files:
connectionProfile.ymlandconnectionState.ymlstay as$refs(single source of truth) - No duplicate maintenance: Changes to core schemas automatically propagate
- CI-friendly: Resolution happens automatically during gradle's
:generatetask.
The Pattern
Committed files (source of truth):
# connectionProfile.yml - KEEP AS $REF
type: object
allOf:
- $ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenProfile.yml'
- $ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenState.yml'
- type: object
x-ui-required:
- accessToken
x-ui-hidden:
- tokenType
- refreshToken
x-oauth-providers:
- vendor.oauth
# connectionState.yml - KEEP AS $REF
$ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenState.yml'
Build-time resolution:
Run zbb generate — it chains syncMeta → assembleSpec → bundleSpec → generate* automatically. bundleSpec natively inlines every $ref in api.yml + connectionProfile.yml (+ optional connectionState.yml); no manual $ref resolution step is needed.
zbb generate
❌ DON'T: Commit Resolved Files
# ❌ WRONG - Committed resolved/flattened file
type: object
properties:
tokenType:
type: string
accessToken:
type: string
# ... all properties inlined
Problems:
- Duplicate maintenance when core schemas change
- Merge conflicts
- Harder to see what's custom vs inherited
✅ DO: Commit $ref Files, Resolve at Build
# ✅ CORRECT - Committed as $ref
$ref: './node_modules/@zerobias-org/types-core/schema/oauthTokenState.yml'
Benefits:
- Single source of truth
- Auto-inherits core schema updates
- Clear distinction between core and custom
Common Mistakes
❌ Creating Custom Profile
# DON'T DO THIS
type: object
properties:
apiKey: string
baseUrl: string
Fix: Extend tokenProfile.yml
❌ Missing baseConnectionState
# DON'T DO THIS
type: object
properties:
accessToken: string
Fix: Extend baseConnectionState or tokenConnectionState
❌ Using expiresAt Instead of expiresIn
# DON'T DO THIS
expiresAt:
type: string
format: date-time
Fix: Convert to expiresIn (seconds) in connect() method
❌ No expiresIn for Refresh Tokens
# DON'T DO THIS
properties:
accessToken: string
refreshToken: string
# Missing expiresIn!
Fix: MUST extend baseConnectionState for expiresIn
Remember
- Always extend core profiles - Never create custom from scratch
- Always extend baseConnectionState - Server needs expiresIn for cronjobs
- expiresIn in seconds - Not timestamps, not milliseconds
- Use allOf for extensions - Add custom fields via composition
- Receive data from @credential-manager - Don't guess auth method
Connection schemas are YAML schemas - design them like api.yml schemas!