diff --git a/ee/apps/den-api/README.md b/ee/apps/den-api/README.md index 0469126f3..0412a7c68 100644 --- a/ee/apps/den-api/README.md +++ b/ee/apps/den-api/README.md @@ -34,6 +34,13 @@ pnpm --filter @openwork-ee/den-api dev:local Each major folder also has its own `README.md` so future agents can inspect one area in isolation. +## TypeID validation + +- Shared Den TypeID validation lives in `ee/packages/utils/src/typeid.ts`. +- Use `typeId.schema("...")` or the compatibility helpers like `normalizeDenTypeId("...", value)` when an endpoint accepts or returns a Den TypeID. +- `ee/apps/den-api/src/openapi.ts` exposes `denTypeIdSchema(...)` so path params, request bodies, and response fields all share the same validation rules and Swagger examples. +- Swagger now documents Den IDs with their required prefix and fixed 26-character TypeID suffix, so invalid IDs fail request validation before route logic runs. + ## Migration approach 1. Keep `den-api` (formerly `den-controller`) as the source of truth for Den control-plane behavior. diff --git a/ee/apps/den-api/src/app.ts b/ee/apps/den-api/src/app.ts index 200b3ac23..f041d54d9 100644 --- a/ee/apps/den-api/src/app.ts +++ b/ee/apps/den-api/src/app.ts @@ -68,6 +68,7 @@ app.get( "/", describeRoute({ tags: ["System"], + hide: true, summary: "Redirect API root", description: "Redirects the API root to the OpenWork marketing site instead of serving API content.", responses: { @@ -110,7 +111,7 @@ registerWorkerRoutes(app) app.get( "/openapi.json", describeRoute({ - tags: ["Documentation"], + tags: ["System"], summary: "Get OpenAPI document", description: "Returns the machine-readable OpenAPI 3.1 document for the Den API so humans and tools can inspect the API surface.", responses: { @@ -123,26 +124,32 @@ app.get( info: { title: "Den API", version: "dev", - description: "OpenAPI spec for the Den control plane API.", + description: [ + "OpenAPI spec for the Den control plane API.", + "", + "Authentication:", + "- Use `Authorization: Bearer ` for user-authenticated routes that require a Den session.", + "- Use `x-api-key: ` for API-key-authenticated routes that accept organization API keys.", + "- Public routes like health and documentation do not require authentication.", + "", + "Swagger tip: use the security schemes in the Authorize dialog to set either `bearerAuth` or `denApiKey` before trying protected endpoints.", + ].join("\n"), }, tags: [ { name: "System", description: "Service health and operational routes." }, - { name: "Documentation", description: "OpenAPI document and Swagger UI routes." }, - { name: "Organizations", description: "Organization-scoped Den API routes." }, - { name: "Organization Invitations", description: "Organization invitation creation, preview, acceptance, and cancellation routes." }, - { name: "Organization API Keys", description: "Organization API key management routes." }, - { name: "Organization Members", description: "Organization member management routes." }, - { name: "Organization Roles", description: "Organization custom role management routes." }, - { name: "Organization Teams", description: "Organization team management routes." }, - { name: "Organization Templates", description: "Organization shared template routes." }, - { name: "Organization LLM Providers", description: "Organization LLM provider catalog, configuration, and access routes." }, - { name: "Organization Skills", description: "Organization skill authoring and sharing routes." }, - { name: "Organization Skill Hubs", description: "Organization skill hub management and access routes." }, + { name: "Organizations", description: "Top-level organization creation and context routes." }, + { name: "Invitations", description: "Invitation preview, acceptance, creation, and cancellation routes." }, + { name: "API Keys", description: "Organization API key management routes." }, + { name: "Members", description: "Organization member management routes." }, + { name: "Roles", description: "Organization custom role management routes." }, + { name: "Teams", description: "Organization team management routes." }, + { name: "Templates", description: "Organization shared template routes." }, + { name: "LLM Providers", description: "Organization LLM provider catalog, configuration, and access routes." }, + { name: "Skills", description: "Organization skill authoring and sharing routes." }, + { name: "Skill Hubs", description: "Organization skill hub management and access routes." }, { name: "Workers", description: "Worker lifecycle, billing, and runtime routes." }, - { name: "Worker Billing", description: "Worker subscription and billing status routes." }, { name: "Worker Runtime", description: "Worker runtime inspection and upgrade routes." }, { name: "Worker Activity", description: "Worker heartbeat and activity reporting routes." }, - { name: "Authentication", description: "Authentication and desktop sign-in handoff routes." }, { name: "Admin", description: "Administrative reporting routes." }, { name: "Users", description: "Current user and membership routes." }, ], @@ -152,11 +159,13 @@ app.get( type: "http", scheme: "bearer", bearerFormat: "session-token", + description: "Session token passed as `Authorization: Bearer ` for user-authenticated Den routes.", }, denApiKey: { type: "apiKey", in: "header", name: "x-api-key", + description: "Organization API key passed as the `x-api-key` header for API-key-authenticated Den routes.", }, }, }, @@ -175,7 +184,7 @@ app.get( app.get( "/docs", describeRoute({ - tags: ["Documentation"], + tags: ["System"], summary: "Serve Swagger UI", description: "Serves Swagger UI so developers can browse and try the Den API from a browser.", responses: { diff --git a/ee/apps/den-api/src/openapi.ts b/ee/apps/den-api/src/openapi.ts index b5330d554..627437883 100644 --- a/ee/apps/den-api/src/openapi.ts +++ b/ee/apps/den-api/src/openapi.ts @@ -1,6 +1,9 @@ +import { type DenTypeIdName, typeId } from "@openwork-ee/utils/typeid" import { resolver } from "hono-openapi" import { z } from "zod" +const TYPE_ID_EXAMPLE_SUFFIX = "01h2xcejqtf2nbrexx3vqjhp41" + function toPascalCase(value: string) { return value .replace(/[^a-zA-Z0-9]+/g, " ") @@ -34,6 +37,15 @@ export function buildOperationId(method: string, path: string) { .replace(/^[A-Z]/, (char) => char.toLowerCase()) } +export function denTypeIdSchema(typeName: TName) { + const prefix = typeId.prefix[typeName] + return typeId.schema(typeName).describe(`Den TypeID with '${prefix}_' prefix.`).meta({ + description: `Den TypeID with '${prefix}_' prefix and a ${typeId.suffixLength}-character base32 suffix.`, + examples: [`${prefix}_${TYPE_ID_EXAMPLE_SUFFIX}`], + format: "typeid", + }) +} + const validationIssueSchema = z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])).optional(), diff --git a/ee/apps/den-api/src/routes/admin/index.ts b/ee/apps/den-api/src/routes/admin/index.ts index f58ceb028..186686a28 100644 --- a/ee/apps/den-api/src/routes/admin/index.ts +++ b/ee/apps/den-api/src/routes/admin/index.ts @@ -6,7 +6,7 @@ import { z } from "zod" import { getCloudWorkerAdminBillingStatus } from "../../billing/polar.js" import { db } from "../../db.js" import { queryValidator, requireAdminMiddleware } from "../../middleware/index.js" -import { invalidRequestSchema, jsonResponse, unauthorizedSchema } from "../../openapi.js" +import { denTypeIdSchema, invalidRequestSchema, jsonResponse, unauthorizedSchema } from "../../openapi.js" import type { AuthContextVariables } from "../../session.js" type UserId = typeof AuthUserTable.$inferSelect.id @@ -17,7 +17,7 @@ const overviewQuerySchema = z.object({ const adminOverviewResponseSchema = z.object({ viewer: z.object({ - id: z.string(), + id: denTypeIdSchema("user"), email: z.string(), name: z.string().nullable(), }), diff --git a/ee/apps/den-api/src/routes/auth/desktop-handoff.ts b/ee/apps/den-api/src/routes/auth/desktop-handoff.ts index 1907b838b..ad838dd50 100644 --- a/ee/apps/den-api/src/routes/auth/desktop-handoff.ts +++ b/ee/apps/den-api/src/routes/auth/desktop-handoff.ts @@ -7,7 +7,7 @@ import { describeRoute } from "hono-openapi" import { z } from "zod" import { jsonValidator, requireUserMiddleware } from "../../middleware/index.js" import { db } from "../../db.js" -import { invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js" +import { denTypeIdSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js" import type { AuthContextVariables } from "../../session.js" const createGrantSchema = z.object({ @@ -28,7 +28,7 @@ const desktopHandoffGrantResponseSchema = z.object({ const desktopHandoffExchangeResponseSchema = z.object({ token: z.string(), user: z.object({ - id: z.string(), + id: denTypeIdSchema("user"), email: z.string().email(), name: z.string().nullable(), }), @@ -115,6 +115,7 @@ export function registerDesktopAuthRoutes(app: Hono) { app.post( "/v1/orgs/:orgId/invitations", describeRoute({ - tags: ["Organizations", "Organization Invitations"], + tags: ["Invitations"], summary: "Create organization invitation", description: "Creates or refreshes a pending organization invitation for an email address and sends the invite email.", responses: { @@ -40,7 +40,7 @@ export function registerOrgInvitationRoutes { if (value.source === "models_dev") { if (!value.providerId) { @@ -482,7 +482,7 @@ export function registerOrgLlmProviderRoutes { const payload = c.get("organizationContext") @@ -1015,7 +1015,7 @@ export function registerOrgLlmProviderRoutes(app: Hono) { app.post( "/v1/orgs/:orgId/members/:memberId/role", describeRoute({ - tags: ["Organizations", "Organization Members"], + tags: ["Members"], summary: "Update member role", description: "Changes the role assigned to a specific organization member.", responses: { 200: jsonResponse("Member role updated successfully.", successSchema), 400: jsonResponse("The member role update request was invalid.", invalidRequestSchema), 401: jsonResponse("The caller must be signed in to update member roles.", unauthorizedSchema), - 403: jsonResponse("Only organization owners can update member roles.", forbiddenSchema), + 403: jsonResponse("Only workspace owners can update member roles.", forbiddenSchema), 404: jsonResponse("The member or organization could not be found.", notFoundSchema), }, }), @@ -83,14 +83,14 @@ export function registerOrgMemberRoutes(app: Hono) { app.post( "/v1/orgs/:orgId/roles", describeRoute({ - tags: ["Organizations", "Organization Roles"], + tags: ["Roles"], summary: "Create organization role", description: "Creates a custom organization role with a named permission map.", responses: { 201: jsonResponse("Organization role created successfully.", successSchema), 400: jsonResponse("The role creation request was invalid.", invalidRequestSchema), 401: jsonResponse("The caller must be signed in to create organization roles.", unauthorizedSchema), - 403: jsonResponse("Only organization owners can create organization roles.", forbiddenSchema), + 403: jsonResponse("Only workspace owners can create custom roles.", forbiddenSchema), 404: jsonResponse("The organization could not be found.", notFoundSchema), }, }), @@ -83,14 +83,14 @@ export function registerOrgRoleRoutes export const orgIdParamSchema = z.object({ - orgId: z.string().trim().min(1).max(255), + orgId: denTypeIdSchema("organization"), }) -export function idParamSchema(key: K) { +export function idParamSchema(key: K, typeName?: DenTypeIdName) { + if (!typeName) { + return z.object({ + [key]: z.string().trim().min(1).max(255), + } as unknown as Record) + } + return z.object({ - [key]: z.string().trim().min(1).max(255), - } as Record) + [key]: denTypeIdSchema(typeName), + } as unknown as Record>) } export function splitRoles(value: string) { @@ -72,7 +79,7 @@ export function ensureOwner(c: { get: (key: "organizationContext") => OrgRouteVa ok: false as const, response: { error: "forbidden", - message: "Only organization owners can manage members and roles.", + message: "Only workspace owners can manage members and roles.", }, } } @@ -99,7 +106,7 @@ export function ensureInviteManager(c: { get: (key: "organizationContext") => Or ok: false as const, response: { error: "forbidden", - message: "Only organization owners and admins can invite members.", + message: "Only workspace owners and admins can invite members.", }, } } @@ -123,7 +130,7 @@ export function ensureTeamManager(c: { get: (key: "organizationContext") => OrgR ok: false as const, response: { error: "forbidden", - message: "Only organization owners and admins can manage teams.", + message: "Only workspace owners and admins can manage teams.", }, } } @@ -147,7 +154,7 @@ export function ensureApiKeyManager(c: { get: (key: "organizationContext") => Or ok: false as const, response: { error: "forbidden", - message: "Only organization owners and admins can manage API keys.", + message: "Only workspace owners and admins can manage API keys.", }, } } diff --git a/ee/apps/den-api/src/routes/org/skills.ts b/ee/apps/den-api/src/routes/org/skills.ts index 2b28a7f7e..380e932c8 100644 --- a/ee/apps/den-api/src/routes/org/skills.ts +++ b/ee/apps/den-api/src/routes/org/skills.ts @@ -22,7 +22,7 @@ import { resolveOrganizationContextMiddleware, } from "../../middleware/index.js" import type { MemberTeamsContext } from "../../middleware/member-teams.js" -import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js" +import { denTypeIdSchema, emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js" import type { OrgRouteVariables } from "./shared.js" import { idParamSchema, memberHasRole, orgIdParamSchema } from "./shared.js" @@ -80,12 +80,12 @@ const updateSkillHubSchema = z.object({ }) const addSkillToHubSchema = z.object({ - skillId: z.string().trim().min(1), + skillId: denTypeIdSchema("skill"), }) const addSkillHubAccessSchema = z.object({ - orgMembershipId: z.string().trim().min(1).optional(), - teamId: z.string().trim().min(1).optional(), + orgMembershipId: denTypeIdSchema("member").optional(), + teamId: denTypeIdSchema("team").optional(), }).superRefine((value, ctx) => { const count = Number(Boolean(value.orgMembershipId)) + Number(Boolean(value.teamId)) if (count !== 1) { @@ -105,10 +105,10 @@ type MemberId = typeof MemberTable.$inferSelect.id type SkillRow = typeof SkillTable.$inferSelect type SkillHubRow = typeof SkillHubTable.$inferSelect -const orgSkillHubParamsSchema = orgIdParamSchema.extend(idParamSchema("skillHubId").shape) -const orgSkillParamsSchema = orgIdParamSchema.extend(idParamSchema("skillId").shape) -const orgSkillHubSkillParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("skillId").shape) -const orgSkillHubAccessParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("accessId").shape) +const orgSkillHubParamsSchema = orgIdParamSchema.extend(idParamSchema("skillHubId", "skillHub").shape) +const orgSkillParamsSchema = orgIdParamSchema.extend(idParamSchema("skillId", "skill").shape) +const orgSkillHubSkillParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("skillId", "skill").shape) +const orgSkillHubAccessParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("accessId", "skillHubMember").shape) const skillResponseSchema = z.object({ skill: z.object({}).passthrough(), @@ -265,7 +265,7 @@ export function registerOrgSkillRoutes { @@ -414,14 +414,14 @@ export function registerOrgSkillRoutes { @@ -822,14 +822,14 @@ export function registerOrgSkillRoutes { if (value.name === undefined && value.memberIds === undefined) { ctx.addIssue({ @@ -45,16 +45,16 @@ const updateTeamSchema = z.object({ type TeamId = typeof TeamTable.$inferSelect.id type MemberId = typeof MemberTable.$inferSelect.id -const orgTeamParamsSchema = orgIdParamSchema.extend(idParamSchema("teamId").shape) +const orgTeamParamsSchema = orgIdParamSchema.extend(idParamSchema("teamId", "team").shape) const teamResponseSchema = z.object({ team: z.object({ - id: z.string(), - organizationId: z.string(), + id: denTypeIdSchema("team"), + organizationId: denTypeIdSchema("organization"), name: z.string(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), - memberIds: z.array(z.string()), + memberIds: z.array(denTypeIdSchema("member")), }), }).meta({ ref: "TeamResponse" }) @@ -87,14 +87,14 @@ export function registerOrgTeamRoutes(app: Hono) { app.post( "/v1/orgs/:orgId/templates", describeRoute({ - tags: ["Organizations", "Organization Templates"], + tags: ["Templates"], summary: "Create shared template", description: "Stores a reusable shared template snapshot inside an organization.", responses: { @@ -103,7 +103,7 @@ export function registerOrgTemplateRoutes diff --git a/ee/packages/utils/package.json b/ee/packages/utils/package.json index ac7a88433..9ff94c156 100644 --- a/ee/packages/utils/package.json +++ b/ee/packages/utils/package.json @@ -21,9 +21,13 @@ "build": "tsup" }, "dependencies": { - "typeid-js": "^1.2.0" + "luxon": "^3.6.1", + "typeid-js": "^1.2.0", + "uuid": "^11.1.0", + "zod": "^4.3.6" }, "devDependencies": { + "@types/luxon": "^3.6.2", "@types/node": "^20.11.30", "tsup": "^8.5.0", "typescript": "^5.5.4" diff --git a/ee/packages/utils/src/typeid.ts b/ee/packages/utils/src/typeid.ts index e9c99a704..4e5a91c4c 100644 --- a/ee/packages/utils/src/typeid.ts +++ b/ee/packages/utils/src/typeid.ts @@ -1,6 +1,13 @@ -import { fromString, getType, typeid } from "typeid-js" +import { DateTime } from "luxon" +import { TypeID, typeid } from "typeid-js" +import { v7 as uuidv7 } from "uuid" +import { z } from "zod" -export const denTypeIdPrefixes = { +export const TYPE_ID_SUFFIX_LENGTH = 26 + +const BASE32_REGEX = /^[0-9a-hjkmnp-tv-z]+$/ + +export const idTypesMapNameToPrefix = { request: "req", user: "usr", session: "ses", @@ -33,40 +40,165 @@ export const denTypeIdPrefixes = { auditEvent: "aev", } as const -export type DenTypeIdName = keyof typeof denTypeIdPrefixes -export type DenTypeIdPrefix = (typeof denTypeIdPrefixes)[TName] -export type DenTypeId = `${DenTypeIdPrefix}_${string}` +export const denTypeIdPrefixes = idTypesMapNameToPrefix + +type IdTypesMapNameToPrefix = typeof idTypesMapNameToPrefix +type IdTypesMapPrefixToName = { + [K in keyof IdTypesMapNameToPrefix as IdTypesMapNameToPrefix[K]]: K +} + +const idTypesMapPrefixToName = Object.fromEntries( + Object.entries(idTypesMapNameToPrefix).map(([name, prefix]) => [prefix, name]), +) as IdTypesMapPrefixToName + +export type IdTypePrefixNames = keyof typeof idTypesMapNameToPrefix +export type DenTypeIdName = IdTypePrefixNames +export type TypeId = `${IdTypesMapNameToPrefix[T]}_${string}` +export type DenTypeId = TypeId + +type TypeIdSchema = z.ZodType, string> + +const schemaCache = new Map>() + +const buildTypeIdSchema = (prefix: T): TypeIdSchema => { + const expectedPrefix = idTypesMapNameToPrefix[prefix] + const expectedLength = TYPE_ID_SUFFIX_LENGTH + expectedPrefix.length + 1 + + return z + .string() + .length(expectedLength, { + message: `TypeID must be ${expectedLength} characters (${expectedPrefix}_<26 char suffix>)`, + }) + .startsWith(`${expectedPrefix}_`, { + message: `TypeID must start with '${expectedPrefix}_'`, + }) + .refine( + (input) => { + const suffix = input.slice(expectedPrefix.length + 1) + return BASE32_REGEX.test(suffix) + }, + { message: "TypeID suffix contains invalid base32 characters" }, + ) + .refine( + (input) => { + try { + TypeID.fromString(input) + return true + } catch { + return false + } + }, + { message: "TypeID is structurally invalid" }, + ) + .transform((input) => TypeID.fromString(input).toString() as TypeId) +} + +const typeIdZodSchema = (prefix: T): TypeIdSchema => { + let schema = schemaCache.get(prefix) + if (!schema) { + schema = buildTypeIdSchema(prefix) + schemaCache.set(prefix, schema) + } + return schema as TypeIdSchema +} + +const typeIdGenerator = ( + prefix: T, +) => typeid(idTypesMapNameToPrefix[prefix]).toString() as TypeId + +const validateTypeId = ( + prefix: T, + data: unknown, +): data is TypeId => typeIdZodSchema(prefix).safeParse(data).success + +const inferTypeId = ( + input: `${T}_${string}`, +): IdTypesMapPrefixToName[T] => { + const parsed = TypeID.fromString(input) + const prefix = parsed.getType() as T + const typeName = idTypesMapPrefixToName[prefix] + + if (typeName === undefined) { + throw new Error( + `Unknown TypeID prefix '${prefix}'. Registered prefixes: ${Object.keys(idTypesMapPrefixToName).join(", ")}`, + ) + } + + return typeName +} + +const typeIdFromString = ( + typeName: T, + input: string, +): TypeId => { + const parsed = TypeID.fromString(input) + const expectedPrefix = idTypesMapNameToPrefix[typeName] + const actualPrefix = parsed.getType() + + if (actualPrefix !== expectedPrefix) { + throw new Error( + `TypeID prefix mismatch: expected '${expectedPrefix}' but got '${actualPrefix}'`, + ) + } + + return parsed.toString() as TypeId +} + +const typeIdWithTimestamp = ( + typeName: T, + timestamp?: Date | number, +): TypeId => { + let msecs: number + + if (timestamp === undefined) { + msecs = DateTime.now().toMillis() + } else if (timestamp instanceof Date) { + msecs = timestamp.getTime() + } else { + msecs = timestamp + } + + if (!Number.isFinite(msecs)) { + throw new Error(`Invalid timestamp: expected finite number, got ${msecs}`) + } + if (msecs < 0) { + throw new Error(`Invalid timestamp: expected non-negative number, got ${msecs}`) + } + + const uuid = uuidv7({ msecs }) + const prefix = idTypesMapNameToPrefix[typeName] + return TypeID.fromUUID(prefix, uuid).toString() as TypeId +} + +const getColumnLength = (typeName: T) => + idTypesMapNameToPrefix[typeName].length + 1 + TYPE_ID_SUFFIX_LENGTH + +export const typeId = { + schema: typeIdZodSchema, + generator: typeIdGenerator, + generatorWithTimestamp: typeIdWithTimestamp, + validator: validateTypeId, + infer: inferTypeId, + fromString: typeIdFromString, + suffixLength: TYPE_ID_SUFFIX_LENGTH, + prefix: idTypesMapNameToPrefix, + columnLength: getColumnLength, +} export function createDenTypeId(name: TName): DenTypeId { - return typeid(denTypeIdPrefixes[name]).toString() as DenTypeId + return typeId.generator(name) } export function normalizeDenTypeId( name: TName, value: string, ): DenTypeId { - const parsed = fromString(value) - const expectedPrefix = denTypeIdPrefixes[name] - - if (getType(parsed) !== expectedPrefix) { - throw new Error(`invalid_den_typeid_prefix:${name}:${getType(parsed)}`) - } - - return parsed as DenTypeId + return typeId.fromString(name, value) } export function isDenTypeId( name: TName, value: unknown, ): value is DenTypeId { - if (typeof value !== "string") { - return false - } - - try { - normalizeDenTypeId(name, value) - return true - } catch { - return false - } + return typeId.validator(name, value) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf7bb58ee..51d511b6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -649,10 +649,22 @@ importers: ee/packages/utils: dependencies: + luxon: + specifier: ^3.6.1 + version: 3.7.2 typeid-js: specifier: ^1.2.0 version: 1.2.0 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: + '@types/luxon': + specifier: ^3.6.2 + version: 3.7.1 '@types/node': specifier: ^20.11.30 version: 20.12.12 @@ -3418,6 +3430,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/luxon@3.7.1': + resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -4777,6 +4792,10 @@ packages: peerDependencies: solid-js: ^1.4.7 + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -9167,6 +9186,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/luxon@3.7.1': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -10500,6 +10521,8 @@ snapshots: dependencies: solid-js: 1.9.9 + luxon@3.7.2: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5