SDK API Reference
The official SDK consists of two framework-agnostic, zero-runtime-dependency packages. The source code is available on GitHub at: https://github.com/gptbots/workspace-extension-sdk.
| Package | Runs in | Entry points |
|---|---|---|
@gptbots/workspace-extension-verify |
Backend (Node ≥ 18) | verifyWsa, exchangeWorkspaceCode |
@gptbots/workspace-extension-sdk |
Browser | consumeHandoff, startWorkspaceLogin, completeWorkspaceLogin … |
Both packages are ESM (
"type": "module"). CommonJS projects should use a dynamicimport()or switch to ESM.
Backend Package @gptbots/workspace-extension-verify
verifyWsa(token, options): WorkspaceIdentity
Verifies the wsa and returns the workspace identity. Verification order: signature → iss → aud → expiration (exp±leeway, including iat/nbf) → required claims.
interface VerifyOptions {
secret?: string; // HS256 secret (per-app or shared). Required when verifying HS256
publicKey?: string; // RS256 PEM public key. Required when verifying RS256 (roadmap)
audience: string; // extension app host, must equal the token's aud (required)
issuer?: string; // default 'gptbots-workspace'
leewaySeconds?: number; // clock drift tolerance (seconds), default 30
algorithms?: ('HS256' | 'RS256')[]; // default ['HS256']
}
interface WorkspaceIdentity {
accountId: string; // = JWT sub
role: 'OWNER' | 'ADMIN' | 'MEMBER'; // unknown values are normalized to MEMBER
workspaceId: string; // = JWT workspace_id
username?: string; email?: string; avatar?: string; appName?: string;
issuedAt?: number; // = iat (seconds)
expiresAt?: number; // = exp (seconds)
}
expenforced: missing or a non-finite number → throwsMissingClaim(never treated as "never expires").iat/nbf(if present) in the future beyond leeway → throwsNotYetValid.- Failures throw
WsaVerificationError(with.code); a caller configuration error (e.g., a missing secret) throwsTypeError.
WsaVerificationError.code values:
| code | Meaning |
|---|---|
InvalidToken |
The token is empty / structurally invalid / a segment is not a JSON object |
InvalidSignature |
The signature does not match (wrong secret, or tampered with) |
Expired |
Already expired (before exp + leeway) |
NotYetValid |
iat/nbf is in the future (beyond leeway) |
WrongIssuer |
iss ≠ the expected value |
WrongAudience |
aud ≠ your audience |
MissingClaim |
Missing exp / sub / role / workspace_id |
UnsupportedAlgorithm |
alg is not in the allowlist (by default only HS256) |
exchangeWorkspaceCode(options): Promise<WorkspaceCodeExchangeResult>
For pull login: on the extension app backend, exchange the one-time code + PKCE codeVerifier for the wsa. Never call this in the browser.
interface ExchangeWorkspaceCodeOptions {
tokenUrl: string; // the platform /token endpoint (absolute URL)
code: string; // the one-time authorization code obtained on callback
codeVerifier: string; // the PKCE verifier corresponding to code_challenge
fetch?: typeof fetch; // injectable fetch (testing/legacy runtimes); default is the global fetch
timeoutMs?: number; // request timeout, default 10000; pass 0 to disable
}
interface WorkspaceCodeExchangeResult {
wsa: string; // the signed wsa, to pass to verifyWsa
tokenType?: string; // 'Bearer'
expiresIn?: number; // wsa validity period (seconds)
}
- Transport failure / non-2xx / a business
code≠0(e.g.,403209 invalid_grant,403210 invalid_verifier) / a missingdata.wsa→ throwsError. - A timeout throws
token exchange timed out after <ms>ms(default 10s, preventing a slow platform response from hanging your backend).
Express Middleware Example
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
export function requireWorkspaceIdentity(secret: string, audience: string) {
return (req, res, next) => {
try {
req.identity = verifyWsa(req.body.wsa, { secret, audience });
next();
} catch (e) {
const code = e instanceof WsaVerificationError ? e.code : 'Error';
res.status(401).json({ code });
}
};
}
Browser Package @gptbots/workspace-extension-sdk
Push (Push handoff)
readHandoffToken(search?, paramName?): string | null
A pure function that reads the raw wsa from the query string. search defaults to location.search, and paramName defaults to 'wsa'.
stripHandoffToken(paramName?, ctx?): void
Uses history.replaceState to remove the wsa from the current URL so it does not linger in the address bar/history/Referer. It is a safe no-op outside the browser. ctx?: { history?, location? } is injectable (testing).
consumeHandoff(options): Promise<WorkspaceIdentity>
A convenience flow for the use level: read the wsa → POST to the extension app backend → on success, strip the wsa → return the identity.
interface ConsumeHandoffOptions {
exchangeUrl: string; // the extension app's backend verification endpoint
fetch?: typeof fetch; // injectable
search?: string; // default location.search
paramName?: string; // default 'wsa'
strip?: boolean; // default true (strips only on success)
}
The token is stripped only after a successful exchange, so that a transient failure can be retried by refreshing. If you want it stripped immediately even on failure, call
stripHandoffToken()manually in thecatchblock.
Pull (Pull / M-Auth)
startWorkspaceLogin(options): Promise<WorkspaceLoginRequest>
Generates PKCE, stores it in sessionStorage, builds the /authorize URL, and (by default) redirects to it. Requires a secure context (HTTPS/localhost).
interface StartWorkspaceLoginOptions {
authorizeUrl: string; // the platform /authorize endpoint (absolute URL)
clientId: string; // the extension app's app home URL
redirectUri: string; // the callback address, host must be same-origin as clientId
state?: string; // default: auto-generate a 16-byte random CSRF state
workspaceId?: string; // preselect a workspace, skipping the organization selection page
storage?: StorageLike; // default sessionStorage
redirect?: (url: string) => void; // default location.assign
navigate?: boolean; // false = only build the URL without redirecting (popup/testing)
}
interface WorkspaceLoginRequest { url: string; state: string; codeVerifier: string; }
readAuthorizeCallback(search?, storage?): AuthorizeCallback | null
On the callback landing page: read code + state, verify state (CSRF), and return code + the stored codeVerifier. It does not consume the stored request (consumption is deferred to a successful exchange), so that a transient failure can be retried by refreshing. Returns null when there is neither a code nor an error.
interface AuthorizeCallback { code: string; state: string | null; codeVerifier: string; }
stripAuthorizeCallback(ctx?): void
Removes code / state from the current URL.
completeWorkspaceLogin(options): Promise<WorkspaceIdentity>
A convenience flow for the use level: read and verify the callback → POST {code, codeVerifier} to your backend → strip the URL on success → return the identity.
interface CompleteWorkspaceLoginOptions {
exchangeUrl: string; // your own backend exchange endpoint
fetch?: typeof fetch;
search?: string; // default location.search
storage?: StorageLike; // default sessionStorage
strip?: boolean; // default true
}
WorkspaceLoginError.code values
| code | Meaning |
|---|---|
NoCallback |
There is no authorization code in the URL |
MissingRequest |
The stored login request is missing/corrupted (call startWorkspaceLogin first) |
StateMismatch |
The state CSRF verification failed |
AuthorizeError |
The callback is an OAuth error redirect (?error=...) |
NoFetch |
No fetch implementation is available |
ExchangeFailed |
The exchange endpoint returned non-2xx |
InvalidResponse |
The exchange endpoint returned invalid JSON |
CryptoUnavailable |
No Web Crypto (requires an HTTPS/localhost secure context) |
StorageUnavailable |
No sessionStorage (cannot store the PKCE verifier) |
InvalidAuthorizeUrl |
authorizeUrl is not an absolute URL |
3. Installing Locally from the Archive
unzip workspace-extension-sdk-0.1.0.zip
# both packages have a prebuilt dist (main=dist/index.js, types=dist/index.d.ts)
npm i ./workspace-extension-sdk/packages/verify # backend
npm i ./workspace-extension-sdk/packages/browser # frontend
To build / run tests yourself:
cd workspace-extension-sdk
npm install
npm run build # tsc → dist for each package
npm test # node --test (zero external test dependencies, requires Node ≥ 22.6 to run .ts directly)
npm run type-check # tsc --noEmit
