@elytracms/studio 0.0.50 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"filter-builder.js","sourceRoot":"","sources":["../../../src/components/cms/filter-builder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { ListFilter, X } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { FilterOperator } from '@elytracms/core/content'\nimport {\n Button,\n Popover,\n PopoverAnchor,\n PopoverContent,\n Row,\n Select,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n OPERATOR_LABELS,\n STATE_FIELD,\n defaultClause,\n filterableFieldOptions,\n operatorsFor,\n scalarListFromText,\n scalarListToText,\n validateClauses,\n} from '@elytracms/studio/lib/cms-views'\nimport type { ClauseIssue, FilterClause, FilterableFieldOption } from '@elytracms/studio/lib/cms-views'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor'\n\n/**\n * A one-line summary of the active filter for the chip face (EC-246b): the\n * single condition read out (\"Status equals news\"), or \"N filters\" for several.\n */\nexport function summarizeFilters(\n filters: readonly FilterClause[],\n options: readonly FilterableFieldOption[],\n): string {\n if (filters.length === 0) return translate('cms.filter.button')\n if (filters.length > 1) return translate('cms.filter.summaryMultiple', { count: filters.length })\n const clause = filters[0]!\n const label = options.find((option) => option.field === clause.field)?.label ?? clause.field\n const value = Array.isArray(clause.value) ? clause.value.join(', ') : String(clause.value)\n return `${label} ${OPERATOR_LABELS[clause.op]} ${value}`.trim()\n}\n\n/**\n * The collection filter entry (EC-246b) — a transient, ad-hoc refine that lives\n * beside the view tabs, not inline. With no active filter it's a compact\n * \"Filter\" button; once refining it's a dashed accent **chip** (distinct from\n * the solid view tabs — a filter is a session lens, not a saved one) whose face\n * opens the condition editor in a **popover** (the table stays visible and\n * updates live) and whose ✕ clears it. Shared/saved views are config-as-code.\n */\nexport function FilterControl({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const filters = source.viewState().filters\n const options = filterableFieldOptions(collection)\n const [open, setOpen] = useState(false)\n\n function addFirstClause() {\n source.setViewState((prev) =>\n prev.filters.length > 0\n ? prev\n : { ...prev, filters: [...prev.filters, defaultClause(options[0]!)] },\n )\n setOpen(true)\n }\n\n function clearAll() {\n source.setViewState((prev) => ({ ...prev, filters: [] }))\n setOpen(false)\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {/* Stable anchor so positioning doesn't jump as the trigger morphs button↔chip. */}\n <PopoverAnchor asChild>\n <div className=\"inline-flex items-center\">\n {filters.length === 0 ? (\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"filter-entry\" onClick={addFirstClause}>\n <ListFilter /> {translate('cms.filter.button')}\n </Button>\n ) : (\n <div\n className={cn(\n 'inline-flex items-center gap-0.5 rounded-md border border-dashed border-primary/40',\n 'bg-primary/5 py-0.5 pl-2 pr-0.5 text-xs',\n )}\n data-testid=\"filter-chip\"\n >\n <button\n type=\"button\"\n className=\"max-w-[220px] truncate text-foreground\"\n onClick={() => setOpen(true)}\n data-testid=\"filter-chip-edit\"\n >\n {summarizeFilters(filters, options)}\n </button>\n <button\n type=\"button\"\n aria-label={translate('cms.filter.clearAria')}\n className=\"rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n onClick={clearAll}\n data-testid=\"filter-chip-clear\"\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n )}\n </div>\n </PopoverAnchor>\n <PopoverContent align=\"start\" className=\"w-[min(92vw,440px)]\">\n <Stack gap=\"2\">\n <Row gap=\"2\" align=\"center\">\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('cms.filter.refineSession')}\n </Text>\n <Spacer />\n {filters.length > 0 ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={clearAll}\n data-testid=\"filter-clear-all\"\n >\n {translate('cms.filter.clearAll')}\n </Button>\n ) : null}\n </Row>\n <FilterBuilder collection={collection} />\n </Stack>\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * The filter builder of the collection table (EC-169). Strictly the closed\n * AD-3 vocabulary: it offers ONLY schema-`filterable` fields (plus the\n * built-in publication state), only `eq`/`in`/`lt`/`lte`/`gt`/`gte`, and\n * conditions combine with AND — there is no OR, no nesting, no free-text\n * query input. Clauses that no longer validate (e.g. a saved view whose field\n * lost its `filterable` flag) are flagged inline and excluded from matching.\n * Rendered inside {@link FilterControl}'s popover (EC-246b).\n */\nexport function FilterBuilder({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const state = source.viewState()\n const options = filterableFieldOptions(collection)\n const issues = validateClauses(collection, state.filters)\n\n // Always update through the previous state — render snapshots go stale\n // across rapid successive events (sort clicks, column toggles).\n function updateFilters(update: (filters: FilterClause[]) => FilterClause[]) {\n source.setViewState((prev) => ({ ...prev, filters: update(prev.filters) }))\n }\n\n return (\n <Row gap=\"2\" wrap align=\"start\" data-testid=\"filter-builder\">\n {state.filters.map((clause, index) => (\n <FilterClauseRow\n key={index}\n clause={clause}\n index={index}\n options={options}\n issue={issues.find((issue) => issue.index === index) ?? null}\n onChange={(next) => updateFilters((filters) => filters.map((c, i) => (i === index ? next : c)))}\n onRemove={() => updateFilters((filters) => filters.filter((_, i) => i !== index))}\n />\n ))}\n <Button\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"add-filter\"\n onClick={() => updateFilters((filters) => [...filters, defaultClause(options[0]!)])}\n >\n <ListFilter /> {translate('cms.filter.add')}\n </Button>\n {state.filters.length > 1 ? (\n <Text as=\"span\" size=\"xs\" tone=\"subtle\" className=\"self-center\">\n {translate('cms.filter.andHint')}\n </Text>\n ) : null}\n </Row>\n )\n}\n\n/** One AND-condition: field · operator · value, plus its validation issue. */\nfunction FilterClauseRow({\n clause,\n index,\n options,\n issue,\n onChange,\n onRemove,\n}: {\n clause: FilterClause\n index: number\n options: FilterableFieldOption[]\n issue: ClauseIssue | null\n onChange: (clause: FilterClause) => void\n onRemove: () => void\n}) {\n const option = options.find((candidate) => candidate.field === clause.field)\n const operators = option ? operatorsFor(option) : [clause.op]\n\n function changeField(field: string) {\n const next = options.find((candidate) => candidate.field === field)\n if (next) onChange(defaultClause(next))\n }\n\n function changeOperator(op: FilterOperator) {\n if (op === 'in') {\n const list = Array.isArray(clause.value) ? clause.value : [clause.value]\n onChange({ ...clause, op, value: list.filter((value) => value !== '') })\n } else {\n const scalar = Array.isArray(clause.value) ? (clause.value[0] ?? '') : clause.value\n onChange({ ...clause, op, value: scalar })\n }\n }\n\n return (\n <Stack gap=\"1\" data-testid={`filter-clause-${index}`}>\n <Row\n gap=\"1\"\n className=\"rounded-md border border-border bg-muted/30 p-1\"\n aria-invalid={issue ? true : undefined}\n >\n <Select\n aria-label={translate('cms.filter.fieldAria')}\n value={option ? clause.field : ''}\n onChange={(event) => changeField(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-field-${index}`}\n >\n {option ? null : (\n <option value=\"\" disabled>\n {clause.field}\n </option>\n )}\n {options.map((candidate) => (\n <option key={candidate.field} value={candidate.field}>\n {candidate.label}\n </option>\n ))}\n </Select>\n <Select\n aria-label={translate('cms.filter.operatorAria')}\n value={clause.op}\n onChange={(event) => changeOperator(event.target.value as FilterOperator)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-op-${index}`}\n >\n {operators.map((op) => (\n <option key={op} value={op}>\n {OPERATOR_LABELS[op]}\n </option>\n ))}\n </Select>\n <ClauseValueEditor\n key={`${clause.field}:${clause.op}`}\n option={option}\n clause={clause}\n index={index}\n onChange={onChange}\n />\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.filter.remove')}\n data-testid={`filter-remove-${index}`}\n onClick={onRemove}\n >\n <X />\n </Button>\n </Row>\n {issue ? (\n <Text size=\"xs\" className=\"text-destructive\" data-testid={`filter-issue-${index}`}>\n {issue.message}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/** The typed value input for a clause (closed per field type and operator). */\nfunction ClauseValueEditor({\n option,\n clause,\n index,\n onChange,\n}: {\n option: FilterableFieldOption | undefined\n clause: FilterClause\n index: number\n onChange: (clause: FilterClause) => void\n}) {\n const testId = `filter-value-${index}`\n\n if (clause.op === 'in') {\n return (\n <DraftValueInput\n ariaLabel={translate('cms.filter.valueAria')}\n initial={scalarListToText(clause.value)}\n placeholder={translate('cms.filter.valueListPlaceholder')}\n testId={testId}\n onText={(raw) => onChange({ ...clause, value: scalarListFromText(option, raw) })}\n />\n )\n }\n\n const scalar = Array.isArray(clause.value) ? (clause.value[0] ?? '') : clause.value\n\n if (clause.field === STATE_FIELD || option?.def === null) {\n return (\n <Select\n aria-label={translate('cms.filter.valueAria')}\n value={String(scalar)}\n onChange={(event) => onChange({ ...clause, value: event.target.value })}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={testId}\n >\n <option value=\"published\">{translate('cms.filter.state.published')}</option>\n <option value=\"draft\">{translate('cms.filter.state.draft')}</option>\n </Select>\n )\n }\n\n // Every concrete scalar field shares the bulk set-field control (EC-247).\n return (\n <ScalarFieldValueEditor\n option={option}\n value={scalar}\n ariaLabel={translate('cms.filter.valueAria')}\n testId={testId}\n onChange={(value) => onChange({ ...clause, value })}\n />\n )\n}\n"]}
1
+ {"version":3,"file":"filter-builder.js","sourceRoot":"","sources":["../../../src/components/cms/filter-builder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { ListFilter, X } from 'lucide-react'\nimport type { CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { FilterScalar } from '@elytracms/core/content'\nimport {\n Button,\n Popover,\n PopoverAnchor,\n PopoverContent,\n Row,\n Select,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n STATE_FIELD,\n defaultClause,\n filterableFieldOptions,\n operatorChoices,\n operatorLabelKey,\n scalarListFromText,\n scalarListToText,\n validateClauses,\n} from '@elytracms/studio/lib/cms-views'\nimport type {\n ClauseIssue,\n FilterClause,\n FilterableFieldOption,\n OperatorChoice,\n} from '@elytracms/studio/lib/cms-views'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { RelationPicker } from './relation-picker'\nimport { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor'\n\n/** How a clause's value reads on the chip face — overridable to resolve relation titles. */\ntype DescribeValue = (clause: FilterClause, option: FilterableFieldOption | undefined) => string\n\nconst defaultDescribeValue: DescribeValue = (clause) =>\n Array.isArray(clause.value) ? clause.value.join(', ') : String(clause.value)\n\n/**\n * A one-line summary of the active filter for the chip face (EC-246b): the single\n * condition read out (\"Status is news\", \"Slug contains hello\", \"Author is Ada\"), or\n * \"N filters\" for several. Operator labels are type-aware and localized (EC-345);\n * `empty` reads as just \"… is empty\" with no value; `describeValue` lets the caller\n * swap raw scalars (e.g. a relation id) for a human title.\n */\nexport function summarizeFilters(\n filters: readonly FilterClause[],\n options: readonly FilterableFieldOption[],\n describeValue: DescribeValue = defaultDescribeValue,\n): string {\n if (filters.length === 0) return translate('cms.filter.button')\n if (filters.length > 1) return translate('cms.filter.summaryMultiple', { count: filters.length })\n const clause = filters[0]!\n const option = options.find((candidate) => candidate.field === clause.field)\n const label = option?.label ?? clause.field\n const opLabel = translate(operatorLabelKey(clause.op, option, clause.value))\n if (clause.op === 'empty') return `${label} ${opLabel}`.trim()\n return `${label} ${opLabel} ${describeValue(clause, option)}`.trim()\n}\n\n/**\n * The collection filter entry (EC-246b) — a transient, ad-hoc refine that lives\n * beside the view tabs, not inline. With no active filter it's a compact\n * \"Filter\" button; once refining it's a dashed accent **chip** (distinct from\n * the solid view tabs — a filter is a session lens, not a saved one) whose face\n * opens the condition editor in a **popover** (the table stays visible and\n * updates live) and whose ✕ clears it. Shared/saved views are config-as-code.\n */\nexport function FilterControl({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const filters = source.viewState().filters\n const options = filterableFieldOptions(collection)\n const [open, setOpen] = useState(false)\n\n // Relation filter values are target ids; read them out as titles on the chip.\n const describeValue: DescribeValue = (clause, option) => {\n if (option?.def?.type === 'relation') {\n const target = option.def.target\n const ids = Array.isArray(clause.value) ? clause.value : [clause.value]\n const titles = ids\n .filter((id) => id !== '' && id != null)\n .map((id) => source.titleOf({ collection: target, id: String(id) }))\n return titles.join(', ')\n }\n return defaultDescribeValue(clause, option)\n }\n\n function addFirstClause() {\n source.setViewState((prev) =>\n prev.filters.length > 0\n ? prev\n : { ...prev, filters: [...prev.filters, defaultClause(options[0]!)] },\n )\n setOpen(true)\n }\n\n function clearAll() {\n source.setViewState((prev) => ({ ...prev, filters: [] }))\n setOpen(false)\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {/* Stable anchor so positioning doesn't jump as the trigger morphs button↔chip. */}\n <PopoverAnchor asChild>\n <div className=\"inline-flex items-center\">\n {filters.length === 0 ? (\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"filter-entry\" onClick={addFirstClause}>\n <ListFilter /> {translate('cms.filter.button')}\n </Button>\n ) : (\n <div\n className={cn(\n 'inline-flex items-center gap-0.5 rounded-md border border-dashed border-primary/40',\n 'bg-primary/5 py-0.5 pl-2 pr-0.5 text-xs',\n )}\n data-testid=\"filter-chip\"\n >\n <button\n type=\"button\"\n className=\"max-w-[220px] truncate text-foreground\"\n onClick={() => setOpen(true)}\n data-testid=\"filter-chip-edit\"\n >\n {summarizeFilters(filters, options, describeValue)}\n </button>\n <button\n type=\"button\"\n aria-label={translate('cms.filter.clearAria')}\n className=\"rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n onClick={clearAll}\n data-testid=\"filter-chip-clear\"\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n )}\n </div>\n </PopoverAnchor>\n <PopoverContent align=\"start\" className=\"w-[min(92vw,440px)]\">\n <Stack gap=\"2\">\n <Row gap=\"2\" align=\"center\">\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('cms.filter.refineSession')}\n </Text>\n <Spacer />\n {filters.length > 0 ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={clearAll}\n data-testid=\"filter-clear-all\"\n >\n {translate('cms.filter.clearAll')}\n </Button>\n ) : null}\n </Row>\n <FilterBuilder collection={collection} />\n </Stack>\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * The filter builder of the collection table (EC-169). Strictly the closed\n * AD-3 vocabulary: it offers ONLY schema-`filterable` fields (plus the\n * built-in publication state), only `eq`/`in`/`lt`/`lte`/`gt`/`gte`, and\n * conditions combine with AND — there is no OR, no nesting, no free-text\n * query input. Clauses that no longer validate (e.g. a saved view whose field\n * lost its `filterable` flag) are flagged inline and excluded from matching.\n * Rendered inside {@link FilterControl}'s popover (EC-246b).\n */\nexport function FilterBuilder({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const state = source.viewState()\n const options = filterableFieldOptions(collection)\n const issues = validateClauses(collection, state.filters)\n\n // Always update through the previous state — render snapshots go stale\n // across rapid successive events (sort clicks, column toggles).\n function updateFilters(update: (filters: FilterClause[]) => FilterClause[]) {\n source.setViewState((prev) => ({ ...prev, filters: update(prev.filters) }))\n }\n\n return (\n <Row gap=\"2\" wrap align=\"start\" data-testid=\"filter-builder\">\n {state.filters.map((clause, index) => (\n <FilterClauseRow\n key={index}\n clause={clause}\n index={index}\n options={options}\n issue={issues.find((issue) => issue.index === index) ?? null}\n onChange={(next) => updateFilters((filters) => filters.map((c, i) => (i === index ? next : c)))}\n onRemove={() => updateFilters((filters) => filters.filter((_, i) => i !== index))}\n />\n ))}\n <Button\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"add-filter\"\n onClick={() => updateFilters((filters) => [...filters, defaultClause(options[0]!)])}\n >\n <ListFilter /> {translate('cms.filter.add')}\n </Button>\n {state.filters.length > 1 ? (\n <Text as=\"span\" size=\"xs\" tone=\"subtle\" className=\"self-center\">\n {translate('cms.filter.andHint')}\n </Text>\n ) : null}\n </Row>\n )\n}\n\n/** One AND-condition: field · operator · value, plus its validation issue. */\nfunction FilterClauseRow({\n clause,\n index,\n options,\n issue,\n onChange,\n onRemove,\n}: {\n clause: FilterClause\n index: number\n options: FilterableFieldOption[]\n issue: ClauseIssue | null\n onChange: (clause: FilterClause) => void\n onRemove: () => void\n}) {\n const option = options.find((candidate) => candidate.field === clause.field)\n const choices = option ? operatorChoices(option) : []\n const currentToken = tokenOfClause(clause)\n\n function changeField(field: string) {\n const next = options.find((candidate) => candidate.field === field)\n if (next) onChange(defaultClause(next))\n }\n\n function changeOperator(token: string) {\n const choice = choices.find((candidate) => candidate.token === token)\n if (!choice) return\n onChange({ ...clause, op: choice.op, value: valueForChoice(choice, clause.value) })\n }\n\n return (\n <Stack gap=\"1\" data-testid={`filter-clause-${index}`}>\n <Row\n gap=\"1\"\n className=\"rounded-md border border-border bg-muted/30 p-1\"\n aria-invalid={issue ? true : undefined}\n >\n <Select\n aria-label={translate('cms.filter.fieldAria')}\n value={option ? clause.field : ''}\n onChange={(event) => changeField(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-field-${index}`}\n >\n {option ? null : (\n <option value=\"\" disabled>\n {clause.field}\n </option>\n )}\n {options.map((candidate) => (\n <option key={candidate.field} value={candidate.field}>\n {candidate.label}\n </option>\n ))}\n </Select>\n <Select\n aria-label={translate('cms.filter.operatorAria')}\n value={currentToken}\n onChange={(event) => changeOperator(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-op-${index}`}\n >\n {choices.length === 0 ? (\n <option value={currentToken} disabled>\n {translate(operatorLabelKey(clause.op, option, clause.value))}\n </option>\n ) : (\n choices.map((choice) => (\n <option key={choice.token} value={choice.token}>\n {translate(choice.labelKey)}\n </option>\n ))\n )}\n </Select>\n <ClauseValueEditor\n key={`${clause.field}:${clause.op}`}\n option={option}\n clause={clause}\n index={index}\n onChange={onChange}\n />\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.filter.remove')}\n data-testid={`filter-remove-${index}`}\n onClick={onRemove}\n >\n <X />\n </Button>\n </Row>\n {issue ? (\n <Text size=\"xs\" className=\"text-destructive\" data-testid={`filter-issue-${index}`}>\n {issue.message}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/** The `<select>` token for a clause's current operator (empty carries its true/false). */\nfunction tokenOfClause(clause: FilterClause): string {\n if (clause.op === 'empty') return clause.value === false ? 'empty:false' : 'empty:true'\n return clause.op\n}\n\n/** The value a clause takes when its operator changes to `choice` (EC-345). */\nfunction valueForChoice(\n choice: OperatorChoice,\n current: FilterScalar | FilterScalar[],\n): FilterScalar | FilterScalar[] {\n if (choice.presetValue !== undefined) return choice.presetValue // empty → true/false\n if (choice.op === 'in') {\n const list = Array.isArray(current) ? current : [current]\n return list.filter((value) => value !== '' && typeof value !== 'boolean')\n }\n // A scalar operator: collapse an array to its first entry; drop a boolean left\n // over from the `empty` operator so the reused value control starts clean.\n const scalar = Array.isArray(current) ? (current[0] ?? '') : current\n return typeof scalar === 'boolean' ? '' : scalar\n}\n\n/** The typed value input for a clause (closed per field type and operator). */\nfunction ClauseValueEditor({\n option,\n clause,\n index,\n onChange,\n}: {\n option: FilterableFieldOption | undefined\n clause: FilterClause\n index: number\n onChange: (clause: FilterClause) => void\n}) {\n const testId = `filter-value-${index}`\n\n // `empty` (is empty / is not empty) carries its verdict in the operator choice —\n // there is no value to enter (EC-345).\n if (clause.op === 'empty') return null\n\n // A relation filter picks its target document(s) via the SAME relation picker the\n // form/inline-cell use — ids are never typed (EC-345). `eq` (incl. membership on a\n // `multiple` relation) picks one; `is any of` (`in`) picks several.\n if (option?.def?.type === 'relation') {\n return (\n <RelationClauseValue\n target={option.def.target}\n clause={clause}\n testId={testId}\n onChange={onChange}\n />\n )\n }\n\n if (clause.op === 'in') {\n return (\n <DraftValueInput\n ariaLabel={translate('cms.filter.valueAria')}\n initial={scalarListToText(clause.value)}\n placeholder={translate('cms.filter.valueListPlaceholder')}\n testId={testId}\n onText={(raw) => onChange({ ...clause, value: scalarListFromText(option, raw) })}\n />\n )\n }\n\n const scalar = Array.isArray(clause.value) ? (clause.value[0] ?? '') : clause.value\n\n if (clause.field === STATE_FIELD || option?.def === null) {\n return (\n <Select\n aria-label={translate('cms.filter.valueAria')}\n value={String(scalar)}\n onChange={(event) => onChange({ ...clause, value: event.target.value })}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={testId}\n >\n <option value=\"published\">{translate('cms.filter.state.published')}</option>\n <option value=\"draft\">{translate('cms.filter.state.draft')}</option>\n </Select>\n )\n }\n\n // Every concrete scalar field shares the bulk set-field control (EC-247).\n return (\n <ScalarFieldValueEditor\n option={option}\n value={scalar}\n ariaLabel={translate('cms.filter.valueAria')}\n testId={testId}\n onChange={(value) => onChange({ ...clause, value })}\n />\n )\n}\n\n/**\n * The relation filter value input (EC-345): a trigger showing the picked target\n * title(s) that opens the existing {@link RelationPicker}. Ids are never typed. For\n * `eq` (a single target, or membership on a `multiple` relation via array-contains,\n * EC-255) it picks ONE; for `is any of` (`in`) it picks several. The stored value is\n * the target id (a string for `eq`, an id array for `in`).\n */\nfunction RelationClauseValue({\n target,\n clause,\n testId,\n onChange,\n}: {\n target: string\n clause: FilterClause\n testId: string\n onChange: (clause: FilterClause) => void\n}) {\n const source = useCollectionView()\n const [open, setOpen] = useState(false)\n const multiPick = clause.op === 'in'\n const ids = (Array.isArray(clause.value) ? clause.value : [clause.value])\n .filter((value) => value !== '' && value != null)\n .map((value) => String(value))\n const refs: DocumentRef[] = ids.map((id) => ({ collection: target, id }))\n const titles = ids.map((id) => source.titleOf({ collection: target, id }))\n\n return (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"h-7 max-w-[200px] truncate py-0 text-xs font-normal\"\n data-testid={testId}\n onClick={() => setOpen(true)}\n >\n {titles.length > 0 ? titles.join(', ') : translate('cms.filter.pickRelation')}\n </Button>\n <RelationPicker\n targetCollection={target}\n multiple={multiPick}\n value={refs}\n open={open}\n onOpenChange={setOpen}\n onChange={(next) =>\n onChange({\n ...clause,\n value: multiPick ? next.map((ref) => ref.id) : (next[0]?.id ?? ''),\n })\n }\n />\n </>\n )\n}\n"]}
@@ -12,6 +12,56 @@ import type { SessionStore } from './local-auth-adapter';
12
12
  * fallback is the neutral "unavailable" branch (never "password too short").
13
13
  */
14
14
  export declare function isDeploymentMisconfigured(message: string): boolean;
15
+ /**
16
+ * Reduce a thrown Convex error to the text used for classification (EC-362).
17
+ *
18
+ * **Read `.data` FIRST, then `.message`** — because a PRODUCTION Convex
19
+ * deployment REDACTS a UDF's thrown-error `.message` to a generic
20
+ * `"[Request ID: …] Server Error"`, but a `ConvexError`'s `.data` payload
21
+ * survives to the client. Proven in the installed `convex@1.42.1` browser
22
+ * client: on an `"error"` response `ConvexHttpClient.action` throws
23
+ * `forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage))`,
24
+ * and `forwardErrorData` sets `error.data = jsonToConvex(errorData)`
25
+ * (`dist/esm/browser/http_client.js`, ~L388-396 + L459-461). The server keeps
26
+ * `errorData` for a `ConvexError` in prod while redacting `errorMessage`. So the
27
+ * only classifiable text that survives prod is the `.data` of a `ConvexError` —
28
+ * e.g. our invite gate's `ConvexError(NOT_INVITED_REASON)`. On DEV deployments
29
+ * nothing is redacted, so `.message` also carries the token (the regex ladder
30
+ * still matches); non-Convex errors only have a `.message`.
31
+ *
32
+ * Duck-types the `data` property rather than `instanceof ConvexError`: two
33
+ * copies of the `convex` module can coexist under Vite (transformed vs
34
+ * pre-bundled), so `instanceof` can spuriously fail (EC-357). `.data` may be a
35
+ * string (our gates) or a structured object (future gates) — both are folded in.
36
+ */
37
+ export declare function convexErrorClassifierText(error: unknown): string;
38
+ type SignInFailure = Extract<SignInResult, {
39
+ ok: false;
40
+ }>;
41
+ /**
42
+ * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362). Runs the
43
+ * regex ladder over {@link convexErrorClassifierText} output (data-first).
44
+ *
45
+ * Order matters: a DEPLOYMENT-misconfiguration (EC-257) and an uninvited
46
+ * sign-up (EC-324) are each checked BEFORE the credential regex so neither
47
+ * degrades to the lying "password too short". The final fallback is HONEST
48
+ * ("unexpected server error — check the deployment logs"), never "unavailable":
49
+ * on a prod deployment the credential/`InvalidAccountId`/env errors thrown by
50
+ * `@convex-dev/auth` are PLAIN `Error`s (not `ConvexError`s), so prod redacts
51
+ * them to `"Server Error"` with no `.data` and they legitimately land here.
52
+ */
53
+ export declare function classifySignInError(flow: 'signIn' | 'signUp', text: string): SignInFailure;
54
+ type ChangePasswordFailure = Extract<ChangePasswordResult, {
55
+ ok: false;
56
+ }>;
57
+ /**
58
+ * Pure password-change error classifier (EC-240 H-tail / EC-362). Same
59
+ * data-first text + honest fallback discipline as {@link classifySignInError};
60
+ * `deployment-misconfigured` collapses onto the `unavailable` reason
61
+ * (`ChangePasswordResult` has no dedicated reason for it) with a specific
62
+ * remediation message.
63
+ */
64
+ export declare function classifyChangePasswordError(text: string): ChangePasswordFailure;
15
65
  /**
16
66
  * Per-deployment token key (EC-179, vision AD-8). One project per backend, so a
17
67
  * session is scoped to its deployment: signing into Aurora's backend must not
@@ -64,3 +114,4 @@ export declare class ConvexAuthAdapter implements AuthAdapter {
64
114
  private toUser;
65
115
  private clearToken;
66
116
  }
117
+ export {};
@@ -35,6 +35,146 @@ const TOKEN_STORAGE_PREFIX = 'elytra.convex-auth.jwt';
35
35
  export function isDeploymentMisconfigured(message) {
36
36
  return /JWT_PRIVATE_KEY|JWKS|CONVEX_SITE_URL|Missing environment variable|PKCS8|private key/i.test(message);
37
37
  }
38
+ /**
39
+ * Reduce a thrown Convex error to the text used for classification (EC-362).
40
+ *
41
+ * **Read `.data` FIRST, then `.message`** — because a PRODUCTION Convex
42
+ * deployment REDACTS a UDF's thrown-error `.message` to a generic
43
+ * `"[Request ID: …] Server Error"`, but a `ConvexError`'s `.data` payload
44
+ * survives to the client. Proven in the installed `convex@1.42.1` browser
45
+ * client: on an `"error"` response `ConvexHttpClient.action` throws
46
+ * `forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage))`,
47
+ * and `forwardErrorData` sets `error.data = jsonToConvex(errorData)`
48
+ * (`dist/esm/browser/http_client.js`, ~L388-396 + L459-461). The server keeps
49
+ * `errorData` for a `ConvexError` in prod while redacting `errorMessage`. So the
50
+ * only classifiable text that survives prod is the `.data` of a `ConvexError` —
51
+ * e.g. our invite gate's `ConvexError(NOT_INVITED_REASON)`. On DEV deployments
52
+ * nothing is redacted, so `.message` also carries the token (the regex ladder
53
+ * still matches); non-Convex errors only have a `.message`.
54
+ *
55
+ * Duck-types the `data` property rather than `instanceof ConvexError`: two
56
+ * copies of the `convex` module can coexist under Vite (transformed vs
57
+ * pre-bundled), so `instanceof` can spuriously fail (EC-357). `.data` may be a
58
+ * string (our gates) or a structured object (future gates) — both are folded in.
59
+ */
60
+ export function convexErrorClassifierText(error) {
61
+ const parts = [];
62
+ if (typeof error === 'object' && error !== null && 'data' in error) {
63
+ const data = error.data;
64
+ if (typeof data === 'string') {
65
+ parts.push(data);
66
+ }
67
+ else if (data !== null && data !== undefined) {
68
+ try {
69
+ parts.push(JSON.stringify(data));
70
+ }
71
+ catch {
72
+ // Non-serializable data (cycles) — ignore; fall back to the message.
73
+ }
74
+ }
75
+ }
76
+ if (error instanceof Error)
77
+ parts.push(error.message);
78
+ else if (typeof error === 'string')
79
+ parts.push(error);
80
+ if (parts.length === 0)
81
+ parts.push(String(error));
82
+ return parts.join('\n');
83
+ }
84
+ /**
85
+ * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362). Runs the
86
+ * regex ladder over {@link convexErrorClassifierText} output (data-first).
87
+ *
88
+ * Order matters: a DEPLOYMENT-misconfiguration (EC-257) and an uninvited
89
+ * sign-up (EC-324) are each checked BEFORE the credential regex so neither
90
+ * degrades to the lying "password too short". The final fallback is HONEST
91
+ * ("unexpected server error — check the deployment logs"), never "unavailable":
92
+ * on a prod deployment the credential/`InvalidAccountId`/env errors thrown by
93
+ * `@convex-dev/auth` are PLAIN `Error`s (not `ConvexError`s), so prod redacts
94
+ * them to `"Server Error"` with no `.data` and they legitimately land here.
95
+ */
96
+ export function classifySignInError(flow, text) {
97
+ // EC-257: a DEPLOYMENT-misconfiguration — its auth signing key is unset — must be
98
+ // classified FIRST so it never falls through to the lying "password too short".
99
+ // Convex Auth signs the session JWT with the deployment's `JWT_PRIVATE_KEY`
100
+ // (+ `JWKS`/`CONVEX_SITE_URL`); when any is unset (`npx @convex-dev/auth` never
101
+ // run) the `requireEnv`/`importPKCS8` call throws AFTER the password is validated,
102
+ // so EVERY sign-in fails regardless of the password.
103
+ if (isDeploymentMisconfigured(text)) {
104
+ return {
105
+ ok: false,
106
+ reason: 'deployment-misconfigured',
107
+ message: 'This deployment can’t sign you in: its auth signing key is not set. Run `npx @convex-dev/auth` against this deployment, then retry. (The deployment-setup check reports the same.)',
108
+ };
109
+ }
110
+ // EC-324: an uninvited sign-up is rejected server-side (`gateAccountCreation`/
111
+ // `gateSessionCreation` throw `ConvexError(NOT_INVITED_REASON)`, whose `.data`
112
+ // survives to the client even in production). Checked BEFORE the credential regex.
113
+ if (/not-invited|invite-only|invite only/i.test(text)) {
114
+ return {
115
+ ok: false,
116
+ reason: 'not-invited',
117
+ message: 'This deployment is invite-only. Ask an admin to invite your email, then create your account.',
118
+ };
119
+ }
120
+ if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(text)) {
121
+ return {
122
+ ok: false,
123
+ reason: 'account-exists',
124
+ message: 'An account with this email already exists — sign in instead.',
125
+ };
126
+ }
127
+ if (/InvalidSecret|InvalidAccountId|credential|password/i.test(text)) {
128
+ return {
129
+ ok: false,
130
+ reason: 'invalid-credentials',
131
+ message: flow === 'signUp'
132
+ ? 'That email or password can’t be used (password too short?).'
133
+ : 'Email or password is incorrect.',
134
+ };
135
+ }
136
+ const verb = flow === 'signUp' ? 'Sign-up' : 'Sign-in';
137
+ return {
138
+ ok: false,
139
+ reason: 'unavailable',
140
+ message: `${verb} failed (unexpected server error) — check the deployment logs.`,
141
+ };
142
+ }
143
+ /**
144
+ * Pure password-change error classifier (EC-240 H-tail / EC-362). Same
145
+ * data-first text + honest fallback discipline as {@link classifySignInError};
146
+ * `deployment-misconfigured` collapses onto the `unavailable` reason
147
+ * (`ChangePasswordResult` has no dedicated reason for it) with a specific
148
+ * remediation message.
149
+ */
150
+ export function classifyChangePasswordError(text) {
151
+ if (isDeploymentMisconfigured(text)) {
152
+ return {
153
+ ok: false,
154
+ reason: 'unavailable',
155
+ message: 'This deployment can’t change your password: its auth signing key is not set. Run `npx @convex-dev/auth`.',
156
+ };
157
+ }
158
+ if (/InvalidSecret|InvalidAccountId|current password|incorrect/i.test(text)) {
159
+ return {
160
+ ok: false,
161
+ reason: 'invalid-credentials',
162
+ message: 'Your current password is incorrect.',
163
+ };
164
+ }
165
+ if (/at least|too short|8 characters|password requirement|weak/i.test(text)) {
166
+ return {
167
+ ok: false,
168
+ reason: 'weak-password',
169
+ message: 'Choose a stronger password (at least 8 characters).',
170
+ };
171
+ }
172
+ return {
173
+ ok: false,
174
+ reason: 'unavailable',
175
+ message: 'Password change failed (unexpected server error) — check the deployment logs.',
176
+ };
177
+ }
38
178
  /**
39
179
  * Per-deployment token key (EC-179, vision AD-8). One project per backend, so a
40
180
  * session is scoped to its deployment: signing into Aurora's backend must not
@@ -120,50 +260,10 @@ export class ConvexAuthAdapter {
120
260
  }
121
261
  catch (error) {
122
262
  // Convex Auth surfaces credential/registration problems as thrown errors.
123
- const message = error instanceof Error ? error.message : String(error);
124
- // EC-257: a DEPLOYMENT-misconfiguration — not a credential problem — must be
125
- // classified FIRST so it never falls through to the lying "password too short".
126
- // Convex Auth signs the session JWT with the deployment's `JWT_PRIVATE_KEY`
127
- // (+ `JWKS`/`CONVEX_SITE_URL`); when any is unset (`npx @convex-dev/auth`
128
- // never run) the `requireEnv`/`importPKCS8` call throws *after* the password
129
- // is validated, so EVERY sign-in fails regardless of the password. This is
130
- // the exact footgun that cost hours on the Konditorei proof (2026-06-22).
131
- if (isDeploymentMisconfigured(message)) {
132
- return {
133
- ok: false,
134
- reason: 'deployment-misconfigured',
135
- message: 'This deployment can’t sign you in: its auth signing key is not set. Run `npx @convex-dev/auth` against this deployment, then retry. (The deployment-setup check reports the same.)',
136
- };
137
- }
138
- // EC-324: an uninvited sign-up is rejected server-side (`beforeSessionCreation`
139
- // throws a `ConvexError('not-invited: …')`, whose data survives to the client
140
- // even in production). Classified BEFORE the credential regex so it never
141
- // degrades to "password too short".
142
- if (/not-invited|invite-only|invite only/i.test(message)) {
143
- return {
144
- ok: false,
145
- reason: 'not-invited',
146
- message: 'This deployment is invite-only. Ask an admin to invite your email, then create your account.',
147
- };
148
- }
149
- if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(message)) {
150
- return {
151
- ok: false,
152
- reason: 'account-exists',
153
- message: 'An account with this email already exists — sign in instead.',
154
- };
155
- }
156
- if (/InvalidSecret|InvalidAccountId|credential|password/i.test(message)) {
157
- return {
158
- ok: false,
159
- reason: 'invalid-credentials',
160
- message: flow === 'signUp'
161
- ? 'That email or password can’t be used (password too short?).'
162
- : 'Email or password is incorrect.',
163
- };
164
- }
165
- const verb = flow === 'signUp' ? 'Sign-up' : 'Sign-in';
166
- return { ok: false, reason: 'unavailable', message: `${verb} is currently unavailable.` };
263
+ // Classify from the DATA-FIRST text (a `ConvexError`'s `.data` survives prod
264
+ // redaction, the redacted `.message` does not — EC-362), then apply the pure
265
+ // regex ladder. See `convexErrorClassifierText` / `classifySignInError`.
266
+ return classifySignInError(flow, convexErrorClassifierText(error));
167
267
  }
168
268
  if (!result.tokens) {
169
269
  return {
@@ -239,29 +339,10 @@ export class ConvexAuthAdapter {
239
339
  return { ok: true };
240
340
  }
241
341
  catch (error) {
242
- const message = error instanceof Error ? error.message : String(error);
243
- if (isDeploymentMisconfigured(message)) {
244
- return {
245
- ok: false,
246
- reason: 'unavailable',
247
- message: 'This deployment can’t change your password: its auth signing key is not set. Run `npx @convex-dev/auth`.',
248
- };
249
- }
250
- if (/InvalidSecret|InvalidAccountId|current password|incorrect/i.test(message)) {
251
- return {
252
- ok: false,
253
- reason: 'invalid-credentials',
254
- message: 'Your current password is incorrect.',
255
- };
256
- }
257
- if (/at least|too short|8 characters|password requirement|weak/i.test(message)) {
258
- return {
259
- ok: false,
260
- reason: 'weak-password',
261
- message: 'Choose a stronger password (at least 8 characters).',
262
- };
263
- }
264
- return { ok: false, reason: 'unavailable', message: 'Password change is currently unavailable.' };
342
+ // Data-first classification (EC-362): the change-password ACTION runs on the
343
+ // same deployment, so a prod redaction erases the thrown `.message` the same
344
+ // way — read a surviving `ConvexError.data` first.
345
+ return classifyChangePasswordError(convexErrorClassifierText(error));
265
346
  }
266
347
  }
267
348
  toUser(me) {
@@ -1 +1 @@
1
- {"version":3,"file":"convex-auth-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/convex-auth-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ConvexHttpClient } from 'convex/browser'\nimport { actionRef, mutationRef, queryRef } from '../persistence/convex-client'\nimport type {\n AuthAdapter,\n AuthSession,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInCredentials,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from './auth-adapter'\nimport type { SessionStore } from './local-auth-adapter'\n\n/**\n * Convex Auth implementation of the project-owned {@link AuthAdapter}\n * (EC-150). Decision record (also in `convex/auth.ts`): **Convex Auth** over\n * Clerk — self-contained on the Convex stack, sessions in our own tables, no\n * extra vendor; reachable only through this adapter so a swap stays local.\n *\n * Flow (server side lives in `convex/auth.ts` + `convex/users.ts`):\n * 1. `signIn` calls the `auth:signIn` action with the Password provider\n * (`flow: 'signIn'`); on success Convex returns a JWT + refresh token.\n * 2. The JWT is set on the *shared* Convex client (`client.setAuth`), so every\n * subsequent persistence query/mutation runs authenticated and the\n * server-side `requireRole` guards see the caller.\n * 3. `currentSession` restores the JWT from storage and asks `users:me`.\n * 4. `signOut` calls `auth:signOut` and clears the JWT.\n *\n * v1 scope note: the stored JWT is short-lived and this adapter does not yet\n * run the refresh-token loop (the official `@convex-dev/auth/react` client\n * does); an expired session simply resolves to \"signed out\" and the user signs\n * in again. Live-deployment hardening is EC-156. No live deployment exists in\n * this repo — this file is typecheck-verified and exercised against the local\n * adapter's shared interface.\n */\n\nconst TOKEN_STORAGE_PREFIX = 'elytra.convex-auth.jwt'\n\n/**\n * Classify a thrown sign-in error as a DEPLOYMENT-setup failure rather than a\n * credential problem (EC-257). Matches the Convex Auth signing-path throws:\n * `requireEnv('JWT_PRIVATE_KEY'|'JWKS'|'CONVEX_SITE_URL')` (→ \"Missing environment\n * variable …\") and `importPKCS8` on a malformed key. Deliberately narrow — a genuine\n * wrong-password error carries none of these tokens — and checked BEFORE the credential\n * regex so it always wins. Note: a PRODUCTION deployment redacts non-`ConvexError`\n * messages to a generic \"Server Error\", which this can't match; there the honest\n * fallback is the neutral \"unavailable\" branch (never \"password too short\").\n */\nexport function isDeploymentMisconfigured(message: string): boolean {\n return /JWT_PRIVATE_KEY|JWKS|CONVEX_SITE_URL|Missing environment variable|PKCS8|private key/i.test(\n message,\n )\n}\n\n/**\n * Per-deployment token key (EC-179, vision AD-8). One project per backend, so a\n * session is scoped to its deployment: signing into Aurora's backend must not\n * carry a JWT to Northwind's. Keying the stored token by the deployment URL\n * makes each backend's session independent — switching to a project on a\n * not-yet-authenticated backend reads no token and the guard asks for sign-in.\n * (A future shared external IdP — WorkOS/Clerk — would issue one identity valid\n * on every deployment, collapsing this back to a single session.)\n */\nexport function tokenStorageKey(deploymentUrl?: string): string {\n return deploymentUrl ? `${TOKEN_STORAGE_PREFIX}::${deploymentUrl}` : TOKEN_STORAGE_PREFIX\n}\n\n/**\n * Best-effort \"is there a session for this deployment?\" — a synchronous token\n * presence check used to label per-backend projects in the switcher/launcher\n * (EC-179). It does not validate the token (an expired one still redirects to\n * sign-in on entry); it just answers \"has the user signed into this backend?\".\n */\nexport function hasStoredConvexSession(\n deploymentUrl: string,\n store: SessionStore = defaultStore(),\n): boolean {\n return store.getItem(tokenStorageKey(deploymentUrl)) !== null\n}\n\ninterface SignInActionResult {\n tokens?: { token: string; refreshToken: string } | null\n}\n\ninterface MeResult {\n id: string\n email: string\n name?: string\n}\n\nfunction defaultStore(): SessionStore {\n if (typeof window !== 'undefined' && window.sessionStorage) return window.sessionStorage\n const memory = new Map<string, string>()\n return {\n getItem: (key) => memory.get(key) ?? null,\n setItem: (key, value) => void memory.set(key, value),\n removeItem: (key) => void memory.delete(key),\n }\n}\n\nexport class ConvexAuthAdapter implements AuthAdapter {\n private readonly tokenKey: string\n\n constructor(\n private readonly client: ConvexHttpClient,\n private readonly store: SessionStore = defaultStore(),\n /** Deployment URL the token is scoped to (per-backend sessions, EC-179). */\n deploymentUrl?: string,\n ) {\n this.tokenKey = tokenStorageKey(deploymentUrl)\n }\n\n async currentSession(): Promise<AuthSession | null> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) return null\n this.client.setAuth(token)\n try {\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return null\n }\n return { user: this.toUser(me) }\n } catch {\n // Expired/invalid JWT or unreachable backend → treat as signed out.\n this.clearToken()\n return null\n }\n }\n\n async signIn(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signIn', credentials)\n }\n\n async signUp(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signUp', credentials)\n }\n\n /**\n * Shared password flow (EC-150/EC-179). `signIn` and `signUp` differ only by\n * the Convex Auth `flow` param; both return a token + load `users:me`.\n */\n private async authenticate(\n flow: 'signIn' | 'signUp',\n credentials: SignInCredentials,\n ): Promise<SignInResult> {\n let result: SignInActionResult\n try {\n result = (await this.client.action(actionRef('auth:signIn'), {\n provider: 'password',\n params: {\n email: credentials.email.trim().toLowerCase(),\n password: credentials.password,\n flow,\n },\n })) as SignInActionResult\n } catch (error) {\n // Convex Auth surfaces credential/registration problems as thrown errors.\n const message = error instanceof Error ? error.message : String(error)\n // EC-257: a DEPLOYMENT-misconfiguration — not a credential problem — must be\n // classified FIRST so it never falls through to the lying \"password too short\".\n // Convex Auth signs the session JWT with the deployment's `JWT_PRIVATE_KEY`\n // (+ `JWKS`/`CONVEX_SITE_URL`); when any is unset (`npx @convex-dev/auth`\n // never run) the `requireEnv`/`importPKCS8` call throws *after* the password\n // is validated, so EVERY sign-in fails regardless of the password. This is\n // the exact footgun that cost hours on the Konditorei proof (2026-06-22).\n if (isDeploymentMisconfigured(message)) {\n return {\n ok: false,\n reason: 'deployment-misconfigured',\n message:\n 'This deployment can’t sign you in: its auth signing key is not set. Run `npx @convex-dev/auth` against this deployment, then retry. (The deployment-setup check reports the same.)',\n }\n }\n // EC-324: an uninvited sign-up is rejected server-side (`beforeSessionCreation`\n // throws a `ConvexError('not-invited: …')`, whose data survives to the client\n // even in production). Classified BEFORE the credential regex so it never\n // degrades to \"password too short\".\n if (/not-invited|invite-only|invite only/i.test(message)) {\n return {\n ok: false,\n reason: 'not-invited',\n message:\n 'This deployment is invite-only. Ask an admin to invite your email, then create your account.',\n }\n }\n if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(message)) {\n return {\n ok: false,\n reason: 'account-exists',\n message: 'An account with this email already exists — sign in instead.',\n }\n }\n if (/InvalidSecret|InvalidAccountId|credential|password/i.test(message)) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message:\n flow === 'signUp'\n ? 'That email or password can’t be used (password too short?).'\n : 'Email or password is incorrect.',\n }\n }\n const verb = flow === 'signUp' ? 'Sign-up' : 'Sign-in'\n return { ok: false, reason: 'unavailable', message: `${verb} is currently unavailable.` }\n }\n if (!result.tokens) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Email or password is incorrect.',\n }\n }\n this.store.setItem(this.tokenKey, result.tokens.token)\n this.client.setAuth(result.tokens.token)\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return { ok: false, reason: 'unavailable', message: 'Sign-in is currently unavailable.' }\n }\n return { ok: true, session: { user: this.toUser(me) } }\n }\n\n async signOut(): Promise<void> {\n try {\n await this.client.action(actionRef('auth:signOut'), {})\n } catch {\n // Already signed out / unreachable — clearing local state is what matters.\n }\n this.clearToken()\n }\n\n /**\n * Self-service identity edit (EC-240 H-tail item 2). Patches the caller's OWN\n * auth `users` row (name / avatar) via the self-scoped `users:updateProfile`\n * mutation, then re-reads `users:me` so the returned session carries the new\n * name. Auth-owned fields never flow through the members-rail `users.save`.\n */\n async updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) {\n return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' }\n }\n this.client.setAuth(token)\n try {\n await this.client.mutation(mutationRef('users:updateProfile'), {\n ...(input.name !== undefined ? { name: input.name } : {}),\n ...(input.avatar !== undefined ? { avatar: input.avatar } : {}),\n })\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' }\n }\n return { ok: true, session: { user: this.toUser(me) } }\n } catch {\n return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' }\n }\n }\n\n /**\n * Self-service password change (EC-240 H-tail item 3). Delegates entirely to\n * the provider via the self-scoped `users:changePassword` ACTION (which calls\n * Convex Auth's `retrieveAccount` to verify the current password, then\n * `modifyAccountCredentials` to set the new one). **Seam only:** the raw\n * password values are forwarded to the provider and NEVER logged or inspected\n * here.\n */\n async changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) {\n return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' }\n }\n this.client.setAuth(token)\n try {\n await this.client.action(actionRef('users:changePassword'), {\n currentPassword: input.currentPassword,\n newPassword: input.newPassword,\n })\n return { ok: true }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (isDeploymentMisconfigured(message)) {\n return {\n ok: false,\n reason: 'unavailable',\n message:\n 'This deployment can’t change your password: its auth signing key is not set. Run `npx @convex-dev/auth`.',\n }\n }\n if (/InvalidSecret|InvalidAccountId|current password|incorrect/i.test(message)) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Your current password is incorrect.',\n }\n }\n if (/at least|too short|8 characters|password requirement|weak/i.test(message)) {\n return {\n ok: false,\n reason: 'weak-password',\n message: 'Choose a stronger password (at least 8 characters).',\n }\n }\n return { ok: false, reason: 'unavailable', message: 'Password change is currently unavailable.' }\n }\n }\n\n private toUser(me: MeResult): AuthSession['user'] {\n return {\n id: me.id,\n email: me.email,\n ...(me.name ? { name: me.name } : {}),\n // No global flags: project creation is config/CLI-owned (AD-11), so there\n // is no in-studio `create-projects` capability. Per-project roles come\n // from `projectMembers`.\n globalFlags: [],\n }\n }\n\n private clearToken(): void {\n this.store.removeItem(this.tokenKey)\n this.client.clearAuth()\n }\n}\n"]}
1
+ {"version":3,"file":"convex-auth-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/convex-auth-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ConvexHttpClient } from 'convex/browser'\nimport { actionRef, mutationRef, queryRef } from '../persistence/convex-client'\nimport type {\n AuthAdapter,\n AuthSession,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInCredentials,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from './auth-adapter'\nimport type { SessionStore } from './local-auth-adapter'\n\n/**\n * Convex Auth implementation of the project-owned {@link AuthAdapter}\n * (EC-150). Decision record (also in `convex/auth.ts`): **Convex Auth** over\n * Clerk — self-contained on the Convex stack, sessions in our own tables, no\n * extra vendor; reachable only through this adapter so a swap stays local.\n *\n * Flow (server side lives in `convex/auth.ts` + `convex/users.ts`):\n * 1. `signIn` calls the `auth:signIn` action with the Password provider\n * (`flow: 'signIn'`); on success Convex returns a JWT + refresh token.\n * 2. The JWT is set on the *shared* Convex client (`client.setAuth`), so every\n * subsequent persistence query/mutation runs authenticated and the\n * server-side `requireRole` guards see the caller.\n * 3. `currentSession` restores the JWT from storage and asks `users:me`.\n * 4. `signOut` calls `auth:signOut` and clears the JWT.\n *\n * v1 scope note: the stored JWT is short-lived and this adapter does not yet\n * run the refresh-token loop (the official `@convex-dev/auth/react` client\n * does); an expired session simply resolves to \"signed out\" and the user signs\n * in again. Live-deployment hardening is EC-156. No live deployment exists in\n * this repo — this file is typecheck-verified and exercised against the local\n * adapter's shared interface.\n */\n\nconst TOKEN_STORAGE_PREFIX = 'elytra.convex-auth.jwt'\n\n/**\n * Classify a thrown sign-in error as a DEPLOYMENT-setup failure rather than a\n * credential problem (EC-257). Matches the Convex Auth signing-path throws:\n * `requireEnv('JWT_PRIVATE_KEY'|'JWKS'|'CONVEX_SITE_URL')` (→ \"Missing environment\n * variable …\") and `importPKCS8` on a malformed key. Deliberately narrow — a genuine\n * wrong-password error carries none of these tokens — and checked BEFORE the credential\n * regex so it always wins. Note: a PRODUCTION deployment redacts non-`ConvexError`\n * messages to a generic \"Server Error\", which this can't match; there the honest\n * fallback is the neutral \"unavailable\" branch (never \"password too short\").\n */\nexport function isDeploymentMisconfigured(message: string): boolean {\n return /JWT_PRIVATE_KEY|JWKS|CONVEX_SITE_URL|Missing environment variable|PKCS8|private key/i.test(\n message,\n )\n}\n\n/**\n * Reduce a thrown Convex error to the text used for classification (EC-362).\n *\n * **Read `.data` FIRST, then `.message`** — because a PRODUCTION Convex\n * deployment REDACTS a UDF's thrown-error `.message` to a generic\n * `\"[Request ID: …] Server Error\"`, but a `ConvexError`'s `.data` payload\n * survives to the client. Proven in the installed `convex@1.42.1` browser\n * client: on an `\"error\"` response `ConvexHttpClient.action` throws\n * `forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage))`,\n * and `forwardErrorData` sets `error.data = jsonToConvex(errorData)`\n * (`dist/esm/browser/http_client.js`, ~L388-396 + L459-461). The server keeps\n * `errorData` for a `ConvexError` in prod while redacting `errorMessage`. So the\n * only classifiable text that survives prod is the `.data` of a `ConvexError` —\n * e.g. our invite gate's `ConvexError(NOT_INVITED_REASON)`. On DEV deployments\n * nothing is redacted, so `.message` also carries the token (the regex ladder\n * still matches); non-Convex errors only have a `.message`.\n *\n * Duck-types the `data` property rather than `instanceof ConvexError`: two\n * copies of the `convex` module can coexist under Vite (transformed vs\n * pre-bundled), so `instanceof` can spuriously fail (EC-357). `.data` may be a\n * string (our gates) or a structured object (future gates) — both are folded in.\n */\nexport function convexErrorClassifierText(error: unknown): string {\n const parts: string[] = []\n if (typeof error === 'object' && error !== null && 'data' in error) {\n const data = (error as { data: unknown }).data\n if (typeof data === 'string') {\n parts.push(data)\n } else if (data !== null && data !== undefined) {\n try {\n parts.push(JSON.stringify(data))\n } catch {\n // Non-serializable data (cycles) — ignore; fall back to the message.\n }\n }\n }\n if (error instanceof Error) parts.push(error.message)\n else if (typeof error === 'string') parts.push(error)\n if (parts.length === 0) parts.push(String(error))\n return parts.join('\\n')\n}\n\ntype SignInFailure = Extract<SignInResult, { ok: false }>\n\n/**\n * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362). Runs the\n * regex ladder over {@link convexErrorClassifierText} output (data-first).\n *\n * Order matters: a DEPLOYMENT-misconfiguration (EC-257) and an uninvited\n * sign-up (EC-324) are each checked BEFORE the credential regex so neither\n * degrades to the lying \"password too short\". The final fallback is HONEST\n * (\"unexpected server error — check the deployment logs\"), never \"unavailable\":\n * on a prod deployment the credential/`InvalidAccountId`/env errors thrown by\n * `@convex-dev/auth` are PLAIN `Error`s (not `ConvexError`s), so prod redacts\n * them to `\"Server Error\"` with no `.data` and they legitimately land here.\n */\nexport function classifySignInError(flow: 'signIn' | 'signUp', text: string): SignInFailure {\n // EC-257: a DEPLOYMENT-misconfiguration — its auth signing key is unset — must be\n // classified FIRST so it never falls through to the lying \"password too short\".\n // Convex Auth signs the session JWT with the deployment's `JWT_PRIVATE_KEY`\n // (+ `JWKS`/`CONVEX_SITE_URL`); when any is unset (`npx @convex-dev/auth` never\n // run) the `requireEnv`/`importPKCS8` call throws AFTER the password is validated,\n // so EVERY sign-in fails regardless of the password.\n if (isDeploymentMisconfigured(text)) {\n return {\n ok: false,\n reason: 'deployment-misconfigured',\n message:\n 'This deployment can’t sign you in: its auth signing key is not set. Run `npx @convex-dev/auth` against this deployment, then retry. (The deployment-setup check reports the same.)',\n }\n }\n // EC-324: an uninvited sign-up is rejected server-side (`gateAccountCreation`/\n // `gateSessionCreation` throw `ConvexError(NOT_INVITED_REASON)`, whose `.data`\n // survives to the client even in production). Checked BEFORE the credential regex.\n if (/not-invited|invite-only|invite only/i.test(text)) {\n return {\n ok: false,\n reason: 'not-invited',\n message:\n 'This deployment is invite-only. Ask an admin to invite your email, then create your account.',\n }\n }\n if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(text)) {\n return {\n ok: false,\n reason: 'account-exists',\n message: 'An account with this email already exists — sign in instead.',\n }\n }\n if (/InvalidSecret|InvalidAccountId|credential|password/i.test(text)) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message:\n flow === 'signUp'\n ? 'That email or password can’t be used (password too short?).'\n : 'Email or password is incorrect.',\n }\n }\n const verb = flow === 'signUp' ? 'Sign-up' : 'Sign-in'\n return {\n ok: false,\n reason: 'unavailable',\n message: `${verb} failed (unexpected server error) — check the deployment logs.`,\n }\n}\n\ntype ChangePasswordFailure = Extract<ChangePasswordResult, { ok: false }>\n\n/**\n * Pure password-change error classifier (EC-240 H-tail / EC-362). Same\n * data-first text + honest fallback discipline as {@link classifySignInError};\n * `deployment-misconfigured` collapses onto the `unavailable` reason\n * (`ChangePasswordResult` has no dedicated reason for it) with a specific\n * remediation message.\n */\nexport function classifyChangePasswordError(text: string): ChangePasswordFailure {\n if (isDeploymentMisconfigured(text)) {\n return {\n ok: false,\n reason: 'unavailable',\n message:\n 'This deployment can’t change your password: its auth signing key is not set. Run `npx @convex-dev/auth`.',\n }\n }\n if (/InvalidSecret|InvalidAccountId|current password|incorrect/i.test(text)) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Your current password is incorrect.',\n }\n }\n if (/at least|too short|8 characters|password requirement|weak/i.test(text)) {\n return {\n ok: false,\n reason: 'weak-password',\n message: 'Choose a stronger password (at least 8 characters).',\n }\n }\n return {\n ok: false,\n reason: 'unavailable',\n message: 'Password change failed (unexpected server error) — check the deployment logs.',\n }\n}\n\n/**\n * Per-deployment token key (EC-179, vision AD-8). One project per backend, so a\n * session is scoped to its deployment: signing into Aurora's backend must not\n * carry a JWT to Northwind's. Keying the stored token by the deployment URL\n * makes each backend's session independent — switching to a project on a\n * not-yet-authenticated backend reads no token and the guard asks for sign-in.\n * (A future shared external IdP — WorkOS/Clerk — would issue one identity valid\n * on every deployment, collapsing this back to a single session.)\n */\nexport function tokenStorageKey(deploymentUrl?: string): string {\n return deploymentUrl ? `${TOKEN_STORAGE_PREFIX}::${deploymentUrl}` : TOKEN_STORAGE_PREFIX\n}\n\n/**\n * Best-effort \"is there a session for this deployment?\" — a synchronous token\n * presence check used to label per-backend projects in the switcher/launcher\n * (EC-179). It does not validate the token (an expired one still redirects to\n * sign-in on entry); it just answers \"has the user signed into this backend?\".\n */\nexport function hasStoredConvexSession(\n deploymentUrl: string,\n store: SessionStore = defaultStore(),\n): boolean {\n return store.getItem(tokenStorageKey(deploymentUrl)) !== null\n}\n\ninterface SignInActionResult {\n tokens?: { token: string; refreshToken: string } | null\n}\n\ninterface MeResult {\n id: string\n email: string\n name?: string\n}\n\nfunction defaultStore(): SessionStore {\n if (typeof window !== 'undefined' && window.sessionStorage) return window.sessionStorage\n const memory = new Map<string, string>()\n return {\n getItem: (key) => memory.get(key) ?? null,\n setItem: (key, value) => void memory.set(key, value),\n removeItem: (key) => void memory.delete(key),\n }\n}\n\nexport class ConvexAuthAdapter implements AuthAdapter {\n private readonly tokenKey: string\n\n constructor(\n private readonly client: ConvexHttpClient,\n private readonly store: SessionStore = defaultStore(),\n /** Deployment URL the token is scoped to (per-backend sessions, EC-179). */\n deploymentUrl?: string,\n ) {\n this.tokenKey = tokenStorageKey(deploymentUrl)\n }\n\n async currentSession(): Promise<AuthSession | null> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) return null\n this.client.setAuth(token)\n try {\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return null\n }\n return { user: this.toUser(me) }\n } catch {\n // Expired/invalid JWT or unreachable backend → treat as signed out.\n this.clearToken()\n return null\n }\n }\n\n async signIn(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signIn', credentials)\n }\n\n async signUp(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signUp', credentials)\n }\n\n /**\n * Shared password flow (EC-150/EC-179). `signIn` and `signUp` differ only by\n * the Convex Auth `flow` param; both return a token + load `users:me`.\n */\n private async authenticate(\n flow: 'signIn' | 'signUp',\n credentials: SignInCredentials,\n ): Promise<SignInResult> {\n let result: SignInActionResult\n try {\n result = (await this.client.action(actionRef('auth:signIn'), {\n provider: 'password',\n params: {\n email: credentials.email.trim().toLowerCase(),\n password: credentials.password,\n flow,\n },\n })) as SignInActionResult\n } catch (error) {\n // Convex Auth surfaces credential/registration problems as thrown errors.\n // Classify from the DATA-FIRST text (a `ConvexError`'s `.data` survives prod\n // redaction, the redacted `.message` does not — EC-362), then apply the pure\n // regex ladder. See `convexErrorClassifierText` / `classifySignInError`.\n return classifySignInError(flow, convexErrorClassifierText(error))\n }\n if (!result.tokens) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Email or password is incorrect.',\n }\n }\n this.store.setItem(this.tokenKey, result.tokens.token)\n this.client.setAuth(result.tokens.token)\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return { ok: false, reason: 'unavailable', message: 'Sign-in is currently unavailable.' }\n }\n return { ok: true, session: { user: this.toUser(me) } }\n }\n\n async signOut(): Promise<void> {\n try {\n await this.client.action(actionRef('auth:signOut'), {})\n } catch {\n // Already signed out / unreachable — clearing local state is what matters.\n }\n this.clearToken()\n }\n\n /**\n * Self-service identity edit (EC-240 H-tail item 2). Patches the caller's OWN\n * auth `users` row (name / avatar) via the self-scoped `users:updateProfile`\n * mutation, then re-reads `users:me` so the returned session carries the new\n * name. Auth-owned fields never flow through the members-rail `users.save`.\n */\n async updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) {\n return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' }\n }\n this.client.setAuth(token)\n try {\n await this.client.mutation(mutationRef('users:updateProfile'), {\n ...(input.name !== undefined ? { name: input.name } : {}),\n ...(input.avatar !== undefined ? { avatar: input.avatar } : {}),\n })\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' }\n }\n return { ok: true, session: { user: this.toUser(me) } }\n } catch {\n return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' }\n }\n }\n\n /**\n * Self-service password change (EC-240 H-tail item 3). Delegates entirely to\n * the provider via the self-scoped `users:changePassword` ACTION (which calls\n * Convex Auth's `retrieveAccount` to verify the current password, then\n * `modifyAccountCredentials` to set the new one). **Seam only:** the raw\n * password values are forwarded to the provider and NEVER logged or inspected\n * here.\n */\n async changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) {\n return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' }\n }\n this.client.setAuth(token)\n try {\n await this.client.action(actionRef('users:changePassword'), {\n currentPassword: input.currentPassword,\n newPassword: input.newPassword,\n })\n return { ok: true }\n } catch (error) {\n // Data-first classification (EC-362): the change-password ACTION runs on the\n // same deployment, so a prod redaction erases the thrown `.message` the same\n // way — read a surviving `ConvexError.data` first.\n return classifyChangePasswordError(convexErrorClassifierText(error))\n }\n }\n\n private toUser(me: MeResult): AuthSession['user'] {\n return {\n id: me.id,\n email: me.email,\n ...(me.name ? { name: me.name } : {}),\n // No global flags: project creation is config/CLI-owned (AD-11), so there\n // is no in-studio `create-projects` capability. Per-project roles come\n // from `projectMembers`.\n globalFlags: [],\n }\n }\n\n private clearToken(): void {\n this.store.removeItem(this.tokenKey)\n this.client.clearAuth()\n }\n}\n"]}