@svadmin/core 0.38.0 → 0.39.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.38.0",
3
+ "version": "0.39.1",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
package/src/audit.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Full AuditLogProvider interface
3
3
 
4
4
  export interface AuditEntry {
5
+ id?: string | number;
5
6
  timestamp: string;
6
7
  action: 'create' | 'update' | 'delete' | 'login' | 'logout';
7
8
  resource?: string;
@@ -11,6 +12,13 @@ export interface AuditEntry {
11
12
  data?: Record<string, unknown>;
12
13
  previousData?: Record<string, unknown>;
13
14
  meta?: Record<string, unknown>;
15
+ outcome?: 'success' | 'failure';
16
+ tenantId?: string | number;
17
+ requestId?: string;
18
+ traceId?: string;
19
+ ipAddress?: string;
20
+ userAgent?: string;
21
+ error?: { message: string; code?: string };
14
22
  }
15
23
 
16
24
  export type AuditHandler = (entry: AuditEntry) => void | Promise<void>;
@@ -22,9 +30,20 @@ export type AuditHandler = (entry: AuditEntry) => void | Promise<void>;
22
30
  * - update: update an existing entry (e.g. mark as reviewed)
23
31
  */
24
32
  export interface AuditLogProvider {
25
- create: (params: { resource: string; action: string; data?: Record<string, unknown>; previousData?: Record<string, unknown>; meta?: Record<string, unknown> }) => Promise<AuditEntry>;
33
+ create: (params: {
34
+ resource: string;
35
+ action: string;
36
+ timestamp?: string;
37
+ recordId?: string | number;
38
+ userId?: string;
39
+ outcome?: AuditEntry['outcome'];
40
+ data?: Record<string, unknown>;
41
+ previousData?: Record<string, unknown>;
42
+ meta?: Record<string, unknown>;
43
+ }) => Promise<AuditEntry>;
26
44
  get: (params: { resource: string; action?: string; meta?: Record<string, unknown>; author?: Record<string, unknown> }) => Promise<AuditEntry[]>;
27
- update: (params: { id: string | number; name: string; meta?: Record<string, unknown> }) => Promise<AuditEntry>;
45
+ /** @deprecated 合规审计应保持追加写入;仅为旧实现兼容保留。 */
46
+ update?: (params: { id: string | number; name: string; meta?: Record<string, unknown> }) => Promise<AuditEntry>;
28
47
  }
29
48
 
30
49
  let handler: AuditHandler = (entry) => {
@@ -68,6 +87,10 @@ export function auditWithProvider(
68
87
  provider.create({
69
88
  resource: entry.resource ?? '',
70
89
  action: entry.action,
90
+ timestamp: fullEntry.timestamp,
91
+ recordId: entry.recordId,
92
+ userId: entry.userId,
93
+ outcome: entry.outcome,
71
94
  data: entry.data ?? entry.details,
72
95
  previousData: entry.previousData,
73
96
  meta: entry.meta,
@@ -78,3 +101,38 @@ export function auditWithProvider(
78
101
  export function audit(entry: Omit<AuditEntry, 'timestamp'>): void {
79
102
  auditWithProvider(entry, auditLogProvider);
80
103
  }
104
+
105
+ /**
106
+ * 严格写入审计记录。与兼容的 best-effort `audit` 不同,此函数会等待所有
107
+ * 写入并传播失败,适合权限、凭据、策略等必须和业务结果一起验证的流程。
108
+ */
109
+ export async function writeAuditEntry(
110
+ entry: Omit<AuditEntry, 'timestamp'>,
111
+ provider: AuditLogProvider | null | undefined = auditLogProvider,
112
+ ): Promise<AuditEntry> {
113
+ const fullEntry: AuditEntry = { ...entry, timestamp: new Date().toISOString() };
114
+ if (!provider) {
115
+ throw new Error('Strict audit logging requires an AuditLogProvider.');
116
+ }
117
+ await handler(fullEntry);
118
+ return provider.create({
119
+ resource: entry.resource ?? '',
120
+ action: entry.action,
121
+ timestamp: fullEntry.timestamp,
122
+ recordId: entry.recordId,
123
+ userId: entry.userId,
124
+ outcome: entry.outcome,
125
+ data: entry.data ?? entry.details,
126
+ previousData: entry.previousData,
127
+ meta: {
128
+ ...(entry.meta ?? {}),
129
+ ...(entry.outcome === undefined ? {} : { outcome: entry.outcome }),
130
+ ...(entry.tenantId === undefined ? {} : { tenantId: entry.tenantId }),
131
+ ...(entry.requestId === undefined ? {} : { requestId: entry.requestId }),
132
+ ...(entry.traceId === undefined ? {} : { traceId: entry.traceId }),
133
+ ...(entry.ipAddress === undefined ? {} : { ipAddress: entry.ipAddress }),
134
+ ...(entry.userAgent === undefined ? {} : { userAgent: entry.userAgent }),
135
+ ...(entry.error === undefined ? {} : { error: entry.error }),
136
+ },
137
+ });
138
+ }
@@ -8,6 +8,7 @@ import type { LiveProvider } from './live.svelte';
8
8
  import type { AccessControlProvider } from './permissions.svelte';
9
9
  import type { AuditLogProvider } from './audit';
10
10
  import type { AgentProvider, ChatProvider } from './chatProvider.svelte';
11
+ import type { EnterpriseRequestContext } from './enterprise';
11
12
  import {
12
13
  createTenantCacheKey,
13
14
  resolveTenantProviderMeta,
@@ -77,6 +78,10 @@ export interface AdminContextValue {
77
78
  readonly notificationProvider: ProviderBundle['notificationProvider'];
78
79
  readonly chatProvider: ChatProvider | null;
79
80
  readonly agentProvider: AgentProvider | null;
81
+ readonly organizationProvider: ProviderBundle['organizationProvider'];
82
+ readonly identityGovernanceProvider: ProviderBundle['identityGovernanceProvider'];
83
+ readonly sessionProvider: ProviderBundle['sessionProvider'];
84
+ readonly credentialProvider: ProviderBundle['credentialProvider'];
80
85
  readonly tenant: TenantContext | undefined;
81
86
  readonly tenantAdapter: TenantAdapter | undefined;
82
87
  }
@@ -102,9 +107,14 @@ export interface AdminContextAccessor {
102
107
  readonly notificationProvider: ProviderBundle['notificationProvider'];
103
108
  readonly chatProvider: ChatProvider | null;
104
109
  readonly agentProvider: AgentProvider | null;
110
+ readonly organizationProvider: ProviderBundle['organizationProvider'];
111
+ readonly identityGovernanceProvider: ProviderBundle['identityGovernanceProvider'];
112
+ readonly sessionProvider: ProviderBundle['sessionProvider'];
113
+ readonly credentialProvider: ProviderBundle['credentialProvider'];
105
114
  readonly tenant: TenantContext | undefined;
106
115
  readonly tenantAdapter: TenantAdapter | undefined;
107
116
  readonly tenantCacheKey: TenantCacheKey | undefined;
117
+ readonly enterpriseRequestContext: EnterpriseRequestContext;
108
118
  getDataProvider(name?: string): DataProvider;
109
119
  getDataProviderNames(): string[];
110
120
  resolveDataProviderName(resourceName: string, overrideName?: string): string;
@@ -166,6 +176,18 @@ function resolveProviderBundle(source: AdminContextSource): ProviderBundle {
166
176
  taskProvider: source.taskProvider ?? bundled?.taskProvider,
167
177
  routerProvider: source.routerProvider ?? bundled?.routerProvider,
168
178
  tenantAdapter: source.tenantAdapter ?? bundled?.tenantAdapter,
179
+ organizationProvider: source.organizationProvider !== undefined
180
+ ? source.organizationProvider
181
+ : bundled?.organizationProvider,
182
+ identityGovernanceProvider: source.identityGovernanceProvider !== undefined
183
+ ? source.identityGovernanceProvider
184
+ : bundled?.identityGovernanceProvider,
185
+ sessionProvider: source.sessionProvider !== undefined
186
+ ? source.sessionProvider
187
+ : bundled?.sessionProvider,
188
+ credentialProvider: source.credentialProvider !== undefined
189
+ ? source.credentialProvider
190
+ : bundled?.credentialProvider,
169
191
  };
170
192
  }
171
193
 
@@ -184,6 +206,10 @@ export function createAdminContext(source: AdminContextSource): AdminContextValu
184
206
  get notificationProvider() { return resolveProviderBundle(source).notificationProvider ?? null; },
185
207
  get chatProvider() { return resolveProviderBundle(source).chatProvider ?? null; },
186
208
  get agentProvider() { return resolveProviderBundle(source).agentProvider ?? null; },
209
+ get organizationProvider() { return resolveProviderBundle(source).organizationProvider ?? null; },
210
+ get identityGovernanceProvider() { return resolveProviderBundle(source).identityGovernanceProvider ?? null; },
211
+ get sessionProvider() { return resolveProviderBundle(source).sessionProvider ?? null; },
212
+ get credentialProvider() { return resolveProviderBundle(source).credentialProvider ?? null; },
187
213
  get tenant() { return source.tenant ?? inheritedTenant; },
188
214
  get tenantAdapter() { return resolveProviderBundle(source).tenantAdapter; },
189
215
  };
@@ -254,11 +280,24 @@ export function captureAdminContext(): AdminContextAccessor {
254
280
  },
255
281
  get chatProvider() { return scopedContext ? scopedContext.chatProvider : getLegacyChatProvider(); },
256
282
  get agentProvider() { return scopedContext ? scopedContext.agentProvider : getLegacyAgentProvider(); },
283
+ get organizationProvider() { return scopedContext?.organizationProvider ?? null; },
284
+ get identityGovernanceProvider() { return scopedContext?.identityGovernanceProvider ?? null; },
285
+ get sessionProvider() { return scopedContext?.sessionProvider ?? null; },
286
+ get credentialProvider() { return scopedContext?.credentialProvider ?? null; },
257
287
  get tenant() { return scopedContext?.tenant; },
258
288
  get tenantAdapter() { return scopedContext?.tenantAdapter; },
259
289
  get tenantCacheKey() {
260
290
  return accessor.tenant ? createTenantCacheKey(accessor.tenant, accessor.tenantAdapter) : undefined;
261
291
  },
292
+ get enterpriseRequestContext() {
293
+ const tenant = accessor.tenant;
294
+ return {
295
+ tenantId: tenant?.tenantId,
296
+ requestId: typeof tenant?.meta?.requestId === 'string' ? tenant.meta.requestId : undefined,
297
+ traceId: typeof tenant?.meta?.traceId === 'string' ? tenant.meta.traceId : undefined,
298
+ meta: tenant?.meta,
299
+ };
300
+ },
262
301
  getDataProvider(name) {
263
302
  const activeProviders = accessor.providers;
264
303
  if (!activeProviders) throw new Error('DataProvider not found. Did you call setDataProvider in App.svelte?');
@@ -0,0 +1,206 @@
1
+ /** 企业级管理能力的正式 Provider 契约。 */
2
+
3
+ export interface EnterpriseActionError {
4
+ message: string;
5
+ code?: string;
6
+ }
7
+
8
+ export interface EnterpriseActionResult {
9
+ success: boolean;
10
+ error?: EnterpriseActionError;
11
+ }
12
+
13
+ /** 每次企业能力调用都必须显式携带当前请求树的租户与追踪上下文。 */
14
+ export interface EnterpriseRequestContext {
15
+ tenantId?: string | number;
16
+ requestId?: string;
17
+ traceId?: string;
18
+ meta?: Readonly<Record<string, unknown>>;
19
+ }
20
+
21
+ /** Provider 调用使用的严格请求上下文;三项边界标识不可省略。 */
22
+ export interface StrictEnterpriseRequestContext {
23
+ tenantId: string | number;
24
+ requestId: string;
25
+ traceId: string;
26
+ meta?: Readonly<Record<string, unknown>>;
27
+ }
28
+
29
+ export interface EnterpriseRequestContextInput {
30
+ tenantId: string | number;
31
+ requestId?: string;
32
+ traceId?: string;
33
+ meta?: Readonly<Record<string, unknown>>;
34
+ }
35
+
36
+ function nonEmpty(value: string, field: string): string {
37
+ const normalized = value.trim();
38
+ if (!normalized) throw new Error(`${field} must not be empty.`);
39
+ return normalized;
40
+ }
41
+
42
+ function validatedTenantId(tenantId: string | number): string | number {
43
+ if (typeof tenantId === 'string') return nonEmpty(tenantId, 'tenantId');
44
+ if (!Number.isFinite(tenantId)) throw new Error('tenantId must be a finite number.');
45
+ return tenantId;
46
+ }
47
+
48
+ function createRequestId(): string {
49
+ const randomUUID = globalThis.crypto?.randomUUID;
50
+ if (!randomUUID) throw new Error('crypto.randomUUID is required to create an enterprise request context.');
51
+ return randomUUID.call(globalThis.crypto);
52
+ }
53
+
54
+ /** Create a validated provider context while preserving one trace across the request tree. */
55
+ export function createEnterpriseRequestContext(
56
+ input: EnterpriseRequestContextInput,
57
+ ): StrictEnterpriseRequestContext {
58
+ const requestId = nonEmpty(input.requestId || createRequestId(), 'requestId');
59
+ return {
60
+ tenantId: validatedTenantId(input.tenantId),
61
+ requestId,
62
+ traceId: nonEmpty(input.traceId || requestId, 'traceId'),
63
+ ...(input.meta ? { meta: input.meta } : {}),
64
+ };
65
+ }
66
+
67
+ /** Narrow a legacy optional context at the provider boundary and fail closed on missing identity. */
68
+ export function assertEnterpriseRequestContext(
69
+ context: EnterpriseRequestContext,
70
+ ): asserts context is StrictEnterpriseRequestContext {
71
+ if (context.tenantId === undefined || context.tenantId === null) {
72
+ throw new Error('tenantId must not be empty.');
73
+ }
74
+ validatedTenantId(context.tenantId);
75
+ nonEmpty(context.requestId ?? '', 'requestId');
76
+ nonEmpty(context.traceId ?? '', 'traceId');
77
+ }
78
+
79
+ export type EnterpriseProviderRequestContext = StrictEnterpriseRequestContext;
80
+
81
+ export interface Organization {
82
+ id: string;
83
+ name: string;
84
+ slug?: string;
85
+ domain?: string;
86
+ metadata?: Record<string, unknown>;
87
+ }
88
+
89
+ export interface OrganizationProvider {
90
+ getCurrentOrganization: (context: EnterpriseRequestContext) => Promise<Organization | null>;
91
+ updateCurrentOrganization?: (
92
+ input: Partial<Omit<Organization, 'id'>>,
93
+ context: EnterpriseRequestContext,
94
+ ) => Promise<Organization>;
95
+ }
96
+
97
+ export interface EnterpriseSecurityPolicy {
98
+ sessionTimeoutMinutes: number;
99
+ auditRetentionDays: number;
100
+ auditLoggingEnabled: boolean;
101
+ requireSso: boolean;
102
+ }
103
+
104
+ export type IdentityProviderProtocol = 'oidc' | 'saml' | (string & {});
105
+ export type IdentityProviderStatus = 'connected' | 'disabled' | 'error' | (string & {});
106
+
107
+ export interface IdentityProviderSummary {
108
+ id: string;
109
+ name: string;
110
+ protocol: IdentityProviderProtocol;
111
+ status: IdentityProviderStatus;
112
+ metadataUrl?: string;
113
+ domain?: string;
114
+ }
115
+
116
+ export interface EnterpriseSecurityEvent {
117
+ id: string;
118
+ event: string;
119
+ actor?: string;
120
+ location?: string;
121
+ createdAt: string | Date;
122
+ severity?: 'info' | 'warning' | 'critical';
123
+ tenantId?: string | number;
124
+ requestId?: string;
125
+ traceId?: string;
126
+ metadata?: Record<string, unknown>;
127
+ }
128
+
129
+ export interface IdentityGovernanceProvider {
130
+ getSecurityPolicy: (context: EnterpriseRequestContext) => Promise<EnterpriseSecurityPolicy>;
131
+ updateSecurityPolicy: (
132
+ policy: EnterpriseSecurityPolicy,
133
+ context: EnterpriseRequestContext,
134
+ ) => Promise<EnterpriseSecurityPolicy>;
135
+ listIdentityProviders: (context: EnterpriseRequestContext) => Promise<IdentityProviderSummary[]>;
136
+ testIdentityProvider: (params: {
137
+ id?: string;
138
+ protocol?: IdentityProviderProtocol;
139
+ metadataUrl?: string;
140
+ }, context: EnterpriseRequestContext) => Promise<EnterpriseActionResult>;
141
+ listSecurityEvents?: (params: {
142
+ page?: number;
143
+ pageSize?: number;
144
+ } | undefined, context: EnterpriseRequestContext) => Promise<{ data: EnterpriseSecurityEvent[]; total: number }>;
145
+ }
146
+
147
+ export interface SessionInfo {
148
+ id: string;
149
+ os?: string;
150
+ browser?: string;
151
+ ipAddress?: string;
152
+ lastActiveAt?: string | Date;
153
+ current: boolean;
154
+ }
155
+
156
+ export interface MfaState {
157
+ enabled: boolean;
158
+ methods?: string[];
159
+ }
160
+
161
+ export interface SessionProvider {
162
+ listSessions: (context: EnterpriseRequestContext) => Promise<SessionInfo[]>;
163
+ revokeSession: (sessionId: string, context: EnterpriseRequestContext) => Promise<EnterpriseActionResult>;
164
+ revokeOtherSessions: (context: EnterpriseRequestContext) => Promise<EnterpriseActionResult>;
165
+ getMfaState?: (context: EnterpriseRequestContext) => Promise<MfaState>;
166
+ setMfaEnabled?: (enabled: boolean, context: EnterpriseRequestContext) => Promise<MfaState>;
167
+ }
168
+
169
+ export interface ApiCredentialSummary {
170
+ id: string;
171
+ name: string;
172
+ prefix: string;
173
+ createdAt: string | Date;
174
+ lastUsedAt?: string | Date;
175
+ permissions: string[];
176
+ }
177
+
178
+ export interface CreatedApiCredential {
179
+ credential: ApiCredentialSummary;
180
+ /** 只在创建时返回一次;列表接口不得返回此字段。 */
181
+ secret: string;
182
+ }
183
+
184
+ export interface WebhookSummary {
185
+ id: string;
186
+ name: string;
187
+ url: string;
188
+ eventType: string;
189
+ enabled?: boolean;
190
+ }
191
+
192
+ export interface CredentialProvider {
193
+ listApiCredentials: (context: EnterpriseRequestContext) => Promise<ApiCredentialSummary[]>;
194
+ createApiCredential: (params: {
195
+ name: string;
196
+ permissions: string[];
197
+ }, context: EnterpriseRequestContext) => Promise<CreatedApiCredential>;
198
+ revokeApiCredential: (credentialId: string, context: EnterpriseRequestContext) => Promise<EnterpriseActionResult>;
199
+ listWebhooks: (context: EnterpriseRequestContext) => Promise<WebhookSummary[]>;
200
+ createWebhook: (params: {
201
+ name: string;
202
+ url: string;
203
+ eventType: string;
204
+ }, context: EnterpriseRequestContext) => Promise<WebhookSummary>;
205
+ deleteWebhook: (webhookId: string, context: EnterpriseRequestContext) => Promise<EnterpriseActionResult>;
206
+ }
@@ -90,6 +90,7 @@ const baseLocales: Record<string, Record<string, string>> = {
90
90
  'common.toggleSidebar': '切换侧边栏',
91
91
  'common.configRequired': '需要配置',
92
92
  'common.removeImage': '移除图片',
93
+ 'common.skipToMainContent': '跳到主要内容',
93
94
  // Field
94
95
  'field.enterValue': '输入{label}',
95
96
  'field.selectPlaceholder': '请选择',
@@ -721,6 +722,7 @@ const baseLocales: Record<string, Record<string, string>> = {
721
722
  'common.toggleSidebar': 'Toggle sidebar',
722
723
  'common.configRequired': 'Configuration Required',
723
724
  'common.removeImage': 'Remove image',
725
+ 'common.skipToMainContent': 'Skip to main content',
724
726
  // Field
725
727
  'field.enterValue': 'Enter {label}',
726
728
  'field.selectPlaceholder': 'Select...',
package/src/index.ts CHANGED
@@ -84,8 +84,32 @@ export {
84
84
  export type { NotificationParams } from './notification.svelte';
85
85
  export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
86
86
  export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
87
- export { audit, auditWithProvider, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
87
+ export { audit, auditWithProvider, writeAuditEntry, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
88
88
  export type { AuditLogProvider } from './audit';
89
+ export type {
90
+ ApiCredentialSummary,
91
+ CreatedApiCredential,
92
+ CredentialProvider,
93
+ EnterpriseActionError,
94
+ EnterpriseActionResult,
95
+ EnterpriseRequestContext,
96
+ EnterpriseRequestContextInput,
97
+ EnterpriseProviderRequestContext,
98
+ StrictEnterpriseRequestContext,
99
+ EnterpriseSecurityEvent,
100
+ EnterpriseSecurityPolicy,
101
+ IdentityGovernanceProvider,
102
+ IdentityProviderProtocol,
103
+ IdentityProviderStatus,
104
+ IdentityProviderSummary,
105
+ MfaState,
106
+ Organization,
107
+ OrganizationProvider,
108
+ SessionInfo,
109
+ SessionProvider,
110
+ WebhookSummary,
111
+ } from './enterprise';
112
+ export { assertEnterpriseRequestContext, createEnterpriseRequestContext } from './enterprise';
89
113
  export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
90
114
  export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
91
115
  export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets, registerThemeOwner, updateThemeOwner, unregisterThemeOwner, resetTheme } from './theme.svelte';
@@ -6,6 +6,12 @@ import type { AuditLogProvider } from './audit';
6
6
  import type { AccessControlProvider } from './permissions.svelte';
7
7
  import type { LiveProvider } from './live.svelte';
8
8
  import type { RouterProvider } from './router-provider';
9
+ import type {
10
+ CredentialProvider,
11
+ IdentityGovernanceProvider,
12
+ OrganizationProvider,
13
+ SessionProvider,
14
+ } from './enterprise';
9
15
  import type {
10
16
  AuthProvider,
11
17
  BaseRecord,
@@ -52,6 +58,10 @@ export interface ProviderBundle {
52
58
  readonly taskProvider?: TaskProvider;
53
59
  readonly routerProvider?: RouterProvider;
54
60
  readonly tenantAdapter?: TenantAdapter;
61
+ readonly organizationProvider?: OrganizationProvider | null;
62
+ readonly identityGovernanceProvider?: IdentityGovernanceProvider | null;
63
+ readonly sessionProvider?: SessionProvider | null;
64
+ readonly credentialProvider?: CredentialProvider | null;
55
65
  }
56
66
 
57
67
  /** 保留传入对象的精确类型,同时执行 ProviderBundle 的编译期校验。 */
@@ -0,0 +1,14 @@
1
+ export type ParsedResourceAction = {
2
+ action: 'list' | 'create' | 'edit' | 'show' | 'clone';
3
+ id?: string;
4
+ };
5
+
6
+ export function parseResourceActionSegments(restSegments: string[]): ParsedResourceAction {
7
+ if (restSegments.length === 0) return { action: 'list' };
8
+ if (restSegments[0] === 'create') return { action: 'create' };
9
+ if (restSegments[0] === 'edit' && restSegments[1]) return { action: 'edit', id: restSegments[1] };
10
+ if (restSegments[0] === 'show' && restSegments[1]) return { action: 'show', id: restSegments[1] };
11
+ if (restSegments[0] === 'clone' && restSegments[1]) return { action: 'clone', id: restSegments[1] };
12
+ if (restSegments[1] === 'edit') return { action: 'edit', id: restSegments[0] };
13
+ return { action: 'show', id: restSegments[0] };
14
+ }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { registerRouterSync } from './router';
5
5
  import { captureAdminContext } from './context.svelte';
6
+ import { parseResourceActionSegments } from './route-parsing';
6
7
 
7
8
  interface ParsedRoute {
8
9
  resource?: string;
@@ -107,47 +108,13 @@ export function useParsed(): ParsedRoute {
107
108
  }
108
109
  }
109
110
 
110
- const restSegments = segments.slice(resourceIndex + 1);
111
- if (restSegments.length === 0) {
112
- result.action = 'list';
113
- } else if (restSegments[0] === 'create') {
114
- result.action = 'create';
115
- } else if (restSegments[0] === 'edit' && restSegments[1]) {
116
- result.action = 'edit';
117
- result.id = restSegments[1];
118
- } else if (restSegments[0] === 'show' && restSegments[1]) {
119
- result.action = 'show';
120
- result.id = restSegments[1];
121
- } else if (restSegments[0] === 'clone' && restSegments[1]) {
122
- result.action = 'clone';
123
- result.id = restSegments[1];
124
- } else if (restSegments[0]) {
125
- // Legacy: /:resource/:id
126
- result.action = 'show';
127
- result.id = restSegments[0];
128
- }
111
+ Object.assign(result, parseResourceActionSegments(segments.slice(resourceIndex + 1)));
129
112
  } else {
130
113
  // Fallback if resource definitions are not loaded yet
131
114
  result.resource = segments[0];
132
115
  result.resourcePath = segments[0];
133
116
 
134
- if (segments.length === 1) {
135
- result.action = 'list';
136
- } else if (segments[1] === 'create') {
137
- result.action = 'create';
138
- } else if (segments[1] === 'edit' && segments[2]) {
139
- result.action = 'edit';
140
- result.id = segments[2];
141
- } else if (segments[1] === 'show' && segments[2]) {
142
- result.action = 'show';
143
- result.id = segments[2];
144
- } else if (segments[1] === 'clone' && segments[2]) {
145
- result.action = 'clone';
146
- result.id = segments[2];
147
- } else if (segments[1]) {
148
- result.action = 'show';
149
- result.id = segments[1];
150
- }
117
+ Object.assign(result, parseResourceActionSegments(segments.slice(1)));
151
118
  }
152
119
 
153
120
  return result;