← Public packages
@kody/doordash-driver
Request DoorDash Drive (Dasher) deliveries with JWT auth from Developer Portal access keys.
src/core.ts
532 lines · 15.9 KB · TypeScript/**
* Shared DoorDash Drive transport: JWT (HS256 + dd-ver) from Developer Portal
* access-key fields, dry-run mutations, setup-aware errors.
*
* Auth model:
* - `developerId` + `keyId` live in package storage via `./settings` (Portal UUIDs).
* - `doorDashSigningSecret` is a Kody secret (Portal `signing_secret`, base64).
* - Live JWTs mint through `kody.secretJwtSign` with algorithm HS256 when the
* platform supports it, or via `createDriveJwt` when raw `signingSecret` is
* passed (local/tools only — never paste the signing secret in chat).
*
* Sandbox and production share openapi.doordash.com; the access key environment
* decides which fleet you hit.
*/
import { kody, packageStorage } from 'kody:runtime'
export const API_BASE_URL = 'https://openapi.doordash.com'
export const API_HOST = 'openapi.doordash.com'
export const DEVELOPER_PORTAL_URL = 'https://developer.doordash.com/portal/auth/credentials'
export const DRIVE_DOCS_URL = 'https://developer.doordash.com/en-US/docs/drive/how_to/JWTs/'
export const DEFAULT_SIGNING_SECRET = 'doorDashSigningSecret'
export const SETTINGS_DEVELOPER_ID = 'developerId'
export const SETTINGS_KEY_ID = 'keyId'
/** Max JWT lifetime DoorDash allows (seconds). Keep tokens shorter in practice. */
export const JWT_MAX_TTL_SECONDS = 1800
export const JWT_DEFAULT_TTL_SECONDS = 300
const SIGNING_SECRET_PATTERN =
/^doorDashSigningSecret(?:-[A-Za-z0-9][A-Za-z0-9_-]{0,47})?$/
const ACCOUNT_LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/
export type DoorDashAuthInput = {
apiBaseUrl?: string
/** Extra app label. `work` reads `doorDashSigningSecret-work`. */
account?: string
signingSecretSecret?: string
/** Portal developer_id UUID. Prefer ./settings; override per call if needed. */
developerId?: string
/** Portal key_id UUID. Prefer ./settings; override per call if needed. */
keyId?: string
/**
* Raw Portal signing_secret (base64). Prefer the Kody secret
* `doorDashSigningSecret` — only pass this in trusted local tooling, never chat.
*/
signingSecret?: string
/** Pre-minted Drive JWT. Skips minting when set. */
jwt?: string
/** JWT lifetime in seconds (1–1800). Default 300. */
jwtTtlSeconds?: number
}
export type DoorDashObject = Record<string, any>
export type DoorDashRateLimitInfo = {
retryAfter: number | null
limit: string | null
remaining: string | null
}
export class DoorDashApiError extends Error {
status: number
code: string | null
details: unknown
headers: Record<string, string>
rateLimit: DoorDashRateLimitInfo
setup: {
signingSecretUrl: string
hosts: string[]
portal: string
settings: string
}
constructor(
message: string,
input: {
status: number
code?: string | null
details?: unknown
headers?: Record<string, string>
auth?: DoorDashAuthInput
},
) {
super(message)
this.name = 'DoorDashApiError'
this.status = input.status
this.code = input.code ?? null
this.details = input.details ?? null
this.headers = input.headers ?? {}
this.rateLimit = parseRateLimitInfo(this.headers)
this.setup = {
signingSecretUrl: signingSecretSetupUrl(resolveSigningSecretName(input.auth)),
hosts: [API_HOST],
portal: DEVELOPER_PORTAL_URL,
settings: 'kody:@kody/doordash-driver/settings',
}
}
}
export function assertNever(value: never, message: string): never {
throw new Error(message + String(value))
}
export function parseAction<T extends string>(
value: unknown,
allowed: readonly T[],
fallback: T,
label: string,
): T {
const action = (value == null || value === '' ? fallback : value) as unknown
if (typeof action === 'string' && (allowed as readonly string[]).includes(action)) {
return action as T
}
throw new Error(
'Unknown ' + label + ' action: ' + String(action) + '. Valid actions: ' + allowed.join(', '),
)
}
function accountLabel(value: string | undefined): string | null {
const trimmed = (value ?? '').trim()
if (!trimmed || trimmed === 'default') return null
if (!ACCOUNT_LABEL_PATTERN.test(trimmed)) {
throw new Error(
'account must be a short label such as "work" or "sandbox" (letters, numbers, _ or -).',
)
}
return trimmed
}
function suffixName(base: string, account: string | null): string {
return account ? base + '-' + account : base
}
function assertSecretName(name: string, pattern: RegExp, expected: string): string {
if (!pattern.test(name)) {
throw new Error(
'Secret name must match ' +
expected +
' (optional -label). Got: ' +
JSON.stringify(name),
)
}
return name
}
/** Resolve the user-scoped signing-secret secret name for this call. */
export function resolveSigningSecretName(input?: DoorDashAuthInput): string {
if (input?.signingSecretSecret != null && String(input.signingSecretSecret).trim() !== '') {
return assertSecretName(
String(input.signingSecretSecret).trim(),
SIGNING_SECRET_PATTERN,
DEFAULT_SIGNING_SECRET,
)
}
return suffixName(DEFAULT_SIGNING_SECRET, accountLabel(input?.account))
}
export function encodeQuery(params: Record<string, string | number | boolean | undefined | null>): string {
const parts: string[] = []
for (const [key, value] of Object.entries(params)) {
if (value == null || value === '') continue
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)))
}
return parts.length ? '?' + parts.join('&') : ''
}
function secretsNewUrl(name: string, description: string): string {
const q =
'name=' +
encodeURIComponent(name) +
'&description=' +
encodeURIComponent(description) +
'&allowedHosts=' +
encodeURIComponent(API_HOST) +
'&scope=user'
return 'https://kody.codes/account/secrets/new?' + q
}
export function signingSecretSetupUrl(name = DEFAULT_SIGNING_SECRET): string {
return secretsNewUrl(
name,
'DoorDash Drive signing_secret (base64) from developer.doordash.com Portal access key JSON',
)
}
export function setupUrls(auth?: DoorDashAuthInput) {
return {
signingSecretUrl: signingSecretSetupUrl(resolveSigningSecretName(auth)),
hosts: [API_HOST] as string[],
portal: DEVELOPER_PORTAL_URL,
docs: DRIVE_DOCS_URL,
settings: 'import settings from "kody:@kody/doordash-driver/settings"',
}
}
function base64UrlEncode(bytes: Uint8Array): string {
let binary = ''
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!)
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
}
function base64UrlEncodeJson(value: unknown): string {
return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)))
}
function decodeBase64ToBytes(value: string): Uint8Array {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4))
const binary = atob(normalized + pad)
const out = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)
return out
}
function normalizeTtl(ttlSeconds?: number): number {
let ttl = ttlSeconds ?? JWT_DEFAULT_TTL_SECONDS
if (!Number.isFinite(ttl) || ttl < 1 || ttl > JWT_MAX_TTL_SECONDS) {
throw new Error('jwtTtlSeconds must be between 1 and ' + JWT_MAX_TTL_SECONDS + '.')
}
return Math.floor(ttl)
}
/**
* Build a DoorDash Drive JWT (HS256, dd-ver DD-JWT-V1) from raw Portal fields.
* Prefer secret-backed minting via `resolveDriveBearerToken` in package code.
*/
export async function createDriveJwt(input: {
developerId: string
keyId: string
signingSecret: string
ttlSeconds?: number
}): Promise<string> {
const developerId = String(input.developerId ?? '').trim()
const keyId = String(input.keyId ?? '').trim()
const signingSecret = String(input.signingSecret ?? '').trim()
if (!developerId || !keyId || !signingSecret) {
throw new Error('developerId, keyId, and signingSecret are required to mint a Drive JWT.')
}
const ttl = normalizeTtl(input.ttlSeconds)
const header = { alg: 'HS256', typ: 'JWT', 'dd-ver': 'DD-JWT-V1' }
const now = Math.floor(Date.now() / 1000)
const payload = {
aud: 'doordash',
iss: developerId,
kid: keyId,
iat: now,
exp: now + ttl,
}
const signingInput = base64UrlEncodeJson(header) + '.' + base64UrlEncodeJson(payload)
const key = await crypto.subtle.importKey(
'raw',
decodeBase64ToBytes(signingSecret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
)
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput))
return signingInput + '.' + base64UrlEncode(new Uint8Array(sig))
}
export async function readDriveIdentity(auth?: DoorDashAuthInput): Promise<{
developerId: string
keyId: string
source: { developerId: string; keyId: string }
}> {
const fromAuthDev = String(auth?.developerId ?? '').trim()
const fromAuthKey = String(auth?.keyId ?? '').trim()
const storage = packageStorage()
const storedDev = String((await storage.get(SETTINGS_DEVELOPER_ID)) ?? '').trim()
const storedKey = String((await storage.get(SETTINGS_KEY_ID)) ?? '').trim()
const developerId = fromAuthDev || storedDev
const keyId = fromAuthKey || storedKey
if (!developerId || !keyId) {
throw new Error(
'Save Portal developer_id and key_id via import settings from "kody:@kody/doordash-driver/settings" ' +
'({ developerId, keyId }), or pass them on the call. Portal: ' +
DEVELOPER_PORTAL_URL,
)
}
return {
developerId,
keyId,
source: {
developerId: fromAuthDev ? 'call' : 'package-storage',
keyId: fromAuthKey ? 'call' : 'package-storage',
},
}
}
/**
* Mint (or reuse) a Drive Bearer token without exposing the signing secret.
* Order: explicit `jwt` → raw `signingSecret` mint → `kody.secretJwtSign` HS256.
*/
export async function resolveDriveBearerToken(auth?: DoorDashAuthInput): Promise<{
token: string
via: 'jwt' | 'raw-signing-secret' | 'secretJwtSign'
signingSecretSecret: string
ttlSeconds: number
developerId: string
keyId: string
}> {
const signingSecretSecret = resolveSigningSecretName(auth)
const ttlSeconds = normalizeTtl(auth?.jwtTtlSeconds)
const explicitJwt = String(auth?.jwt ?? '').trim()
if (explicitJwt) {
const { developerId, keyId } = await readDriveIdentity(auth).catch(() => ({
developerId: '',
keyId: '',
}))
return {
token: explicitJwt,
via: 'jwt',
signingSecretSecret,
ttlSeconds,
developerId,
keyId,
}
}
const { developerId, keyId } = await readDriveIdentity(auth)
const rawSigning = String(auth?.signingSecret ?? '').trim()
if (rawSigning) {
const token = await createDriveJwt({
developerId,
keyId,
signingSecret: rawSigning,
ttlSeconds,
})
return {
token,
via: 'raw-signing-secret',
signingSecretSecret,
ttlSeconds,
developerId,
keyId,
}
}
const now = Math.floor(Date.now() / 1000)
try {
const signed = await kody.secretJwtSign({
private_key_secret_name: signingSecretSecret,
algorithm: 'HS256',
key_encoding: 'base64',
header: { 'dd-ver': 'DD-JWT-V1' },
claims: {
aud: 'doordash',
iss: developerId,
kid: keyId,
iat: now,
exp: now + ttlSeconds,
},
})
const token = String((signed as { jwt?: string })?.jwt ?? '').trim()
if (!token) {
throw new Error('secretJwtSign returned no jwt')
}
return {
token,
via: 'secretJwtSign',
signingSecretSecret,
ttlSeconds,
developerId,
keyId,
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
throw new Error(
'Could not mint a DoorDash Drive JWT with secret ' +
signingSecretSecret +
'. Save the Portal signing_secret (base64) at ' +
signingSecretSetupUrl(signingSecretSecret) +
' and approve host ' +
API_HOST +
'. Drive JWTs use HS256; the platform secretJwtSign path must allow HS256 with base64 key material. ' +
'Detail: ' +
message,
)
}
}
export function parseRateLimitInfo(headers: Record<string, string>): DoorDashRateLimitInfo {
const lower: Record<string, string> = {}
for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v
const retryRaw = lower['retry-after']
const retryAfter =
retryRaw != null && retryRaw !== '' && Number.isFinite(Number(retryRaw))
? Number(retryRaw)
: null
return {
retryAfter,
limit: lower['x-ratelimit-limit'] ?? lower['ratelimit-limit'] ?? null,
remaining: lower['x-ratelimit-remaining'] ?? lower['ratelimit-remaining'] ?? null,
}
}
function headersToObject(headers: Headers): Record<string, string> {
const out: Record<string, string> = {}
headers.forEach((value, key) => {
out[key] = value
})
return out
}
export type DoorDashRequestResult<T = DoorDashObject> = {
ok: true
status: number
data: T
headers: Record<string, string>
rateLimit: DoorDashRateLimitInfo
}
export type DoorDashRequestInput = DoorDashAuthInput & {
method?: string
path: string
query?: Record<string, string | number | boolean | undefined | null>
body?: unknown
/** When true, return the preview without calling DoorDash. */
dryRun?: boolean
/**
* Mutations (POST/PUT/PATCH/DELETE) default to dry-run. Pass confirm: true
* to execute live. GET/HEAD never require confirm.
*/
confirm?: boolean
}
function isMutation(method: string): boolean {
return method !== 'GET' && method !== 'HEAD'
}
/**
* Signed request to DoorDash Drive (`openapi.doordash.com`).
* Mutations are dry-run unless `confirm: true`.
*/
export async function doorDashRequest<T = DoorDashObject>(
input: DoorDashRequestInput,
): Promise<DoorDashRequestResult<T> | { ok: true; dryRun: true; preview: DoorDashObject }> {
const method = String(input.method ?? 'GET').toUpperCase()
const path = String(input.path ?? '').trim()
if (!path.startsWith('/')) {
throw new Error('path must start with / (for example /drive/v2/deliveries).')
}
const base = String(input.apiBaseUrl ?? API_BASE_URL).replace(/\/+$/, '')
const url = base + path + encodeQuery(input.query ?? {})
const dryRunExplicit = input.dryRun === true
const confirm = input.confirm === true
const shouldDryRun =
dryRunExplicit || (isMutation(method) && !confirm && input.dryRun !== false)
const signingSecretSecret = resolveSigningSecretName(input)
if (shouldDryRun) {
let identity: { developerId: string; keyId: string } | null = null
try {
identity = await readDriveIdentity(input)
} catch {
identity = null
}
return {
ok: true,
dryRun: true,
preview: {
method,
url,
auth: 'DoorDash Drive JWT (HS256, dd-ver DD-JWT-V1)',
signingSecretSecret,
developerId: identity?.developerId ?? null,
keyId: identity?.keyId ?? null,
body: input.body ?? null,
setup: setupUrls(input),
note:
isMutation(method) && !confirm
? 'Mutation preview only. Pass confirm: true to call DoorDash.'
: 'dryRun preview — no network call.',
},
}
}
const minted = await resolveDriveBearerToken(input)
const headers: Record<string, string> = {
Authorization: 'Bearer ' + minted.token,
Accept: 'application/json',
}
let bodyText: string | undefined
if (input.body !== undefined && input.body !== null && method !== 'GET' && method !== 'HEAD') {
headers['Content-Type'] = 'application/json'
bodyText = JSON.stringify(input.body)
}
const response = await fetch(url, { method, headers, body: bodyText })
const responseHeaders = headersToObject(response.headers)
const text = await response.text()
let parsed: unknown = null
if (text) {
try {
parsed = JSON.parse(text)
} catch {
parsed = { raw: text }
}
}
if (!response.ok) {
const obj = parsed && typeof parsed === 'object' ? (parsed as DoorDashObject) : null
const code =
(obj && (obj.code || obj.errorCode || obj.error_code || obj.type)) != null
? String(obj.code || obj.errorCode || obj.error_code || obj.type)
: null
const message =
(obj && (obj.message || obj.error || obj.detail)) != null
? String(obj.message || obj.error || obj.detail)
: 'DoorDash API error ' + response.status
throw new DoorDashApiError(message, {
status: response.status,
code,
details: parsed,
headers: responseHeaders,
auth: input,
})
}
return {
ok: true,
status: response.status,
data: (parsed ?? {}) as T,
headers: responseHeaders,
rateLimit: parseRateLimitInfo(responseHeaders),
}
}