{"record":{"id":"0727523aabc6dcea","repo":"hcengineering/platform","slug":"nbf-and-exp-are-required-when-sub-is-not-provided","errorCode":null,"errorMessage":"nbf and exp are required when sub is not provided","messagePattern":"nbf and exp are required when sub is not provided","errorType":"exception","errorClass":"TokenError","httpStatus":null,"severity":"error","filePath":"foundations/core/packages/token/src/token.ts","lineNumber":79,"sourceCode":"    grant?: PermissionsGrant\n    nbf?: number\n    exp?: number\n    sub?: PersonUuid\n  }\n): string {\n  if (!validate(accountUuid)) {\n    throw new TokenError(`Invalid account uuid: \"${accountUuid}\"`)\n  }\n  if (workspaceUuid !== undefined && !validate(workspaceUuid)) {\n    throw new TokenError(`Invalid workspace uuid: \"${workspaceUuid}\"`)\n  }\n  const { grant, nbf, exp, sub } = options ?? {}\n  if (grant?.workspace !== undefined && !validate(grant?.workspace)) {\n    throw new TokenError(`Invalid grant workspace uuid: \"${grant?.workspace}\"`)\n  }\n\n  if (grant != null && sub == null && (nbf == null || exp == null)) {\n    throw new TokenError('nbf and exp are required when sub is not provided')\n  }\n\n  const service = getMetadata(serverPlugin.metadata.Service)\n  if (service !== undefined) {\n    extra = { service, ...extra }\n  }\n\n  const sanitizedGrant: PermissionsGrant | undefined =\n    grant !== undefined\n      ? {\n          workspace: grant.workspace,\n          role: grant.role,\n          grantedBy: grant.grantedBy,\n          firstName: grant.firstName,\n          lastName: grant.lastName,\n          spaces: grant.spaces,\n          extra: grant.extra\n        }","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/foundations/core/packages/token/src/token.ts#L61-L97","documentation":"A grant with no sub (subject) must be time-bounded: generateToken requires both nbf and exp when a grant is given but sub is omitted, so anonymous/workspace-grant tokens cannot live forever.","triggerScenarios":"Calling generateToken with grant set, sub left undefined, and either nbf or exp (or both) missing from options.","commonSituations":"Issuing service or workspace-scoped tokens where the developer forgot expiry; migrating code that previously used sub and now omits it without adding nbf/exp.","solutions":["Provide both nbf and exp in options when issuing a grant token without sub","Set sub (a PersonUuid) if the token should be bound to a person instead","Review token-issuing call sites to ensure grant tokens always have a validity window"],"exampleFix":"// before\ngenerateToken(account, workspace, { grant: { workspace } })\n// after\nconst now = Math.floor(Date.now() / 1000)\ngenerateToken(account, workspace, { grant: { workspace }, nbf: now, exp: now + 3600 })","handlingStrategy":"validation","validationCode":"if (grant != null && sub == null && (nbf == null || exp == null)) {\n  throw new Error('provide nbf and exp for grant tokens without sub')\n}","typeGuard":null,"tryCatchPattern":"try {\n  return generateToken(account, workspace, opts)\n} catch (e) {\n  if (e instanceof TokenError && e.message.includes('nbf and exp are required')) {\n    const now = Math.floor(Date.now() / 1000)\n    return generateToken(account, workspace, { ...opts, nbf: now, exp: now + opts.ttl ?? 3600 })\n  }\n  throw e\n}","preventionTips":["Wrap token issuance in a helper that always injects a default validity window","Remember sub OR (nbf+exp) is mandatory whenever a grant is present","Set token TTLs explicitly in one shared constant"],"tags":["token","jwt","validation","expiry"],"backgroundTag":"jwt-missing-expiry-claim","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}