kanban-lite 1.2.3 → 1.2.4

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.
@@ -468,29 +468,11 @@ export declare class CardStateError extends Error {
468
468
  constructor(code: CardStateErrorCode, message: string);
469
469
  }
470
470
  /**
471
- * Minimal SDK webhook facade supplied to CLI plugins via {@link CliPluginContext}.
472
- *
473
- * Structural subset of `KanbanSDK`; plugins should use this surface instead of
474
- * importing `KanbanSDK` directly so they remain decoupled from core internals.
471
+ * @deprecated Use {@link KanbanSDK}. CLI plugin hosts now advertise the full
472
+ * public SDK surface, including `getConfigSnapshot()`, instead of a narrowed
473
+ * webhook-only facade.
475
474
  */
476
- export interface CliPluginSdk {
477
- /**
478
- * Returns the SDK extension bag contributed by the plugin with the given id,
479
- * when the host is backed by a full `KanbanSDK` instance.
480
- *
481
- * CLI plugins should prefer this extension path when available and fall back
482
- * to compatibility methods only when running against older or mocked SDK facades.
483
- */
484
- getExtension?<T extends Record<string, unknown> = Record<string, unknown>>(id: string): T | undefined;
485
- listWebhooks(): Webhook[];
486
- createWebhook(input: {
487
- url: string;
488
- events: string[];
489
- secret?: string;
490
- }): Promise<Webhook>;
491
- updateWebhook(id: string, updates: Partial<Pick<Webhook, 'url' | 'events' | 'secret' | 'active'>>): Promise<Webhook | null>;
492
- deleteWebhook(id: string): Promise<boolean>;
493
- }
475
+ export type CliPluginSdk = KanbanSDK;
494
476
  /**
495
477
  * Runtime context supplied to a {@link KanbanCliPlugin} when it is invoked by
496
478
  * the `kl` CLI.
@@ -503,10 +485,12 @@ export interface CliPluginContext {
503
485
  *
504
486
  * Present when the plugin is invoked through the core `kl` CLI.
505
487
  * Absent in isolated unit tests or standalone invocations.
506
- * Plugins should prefer this over constructing their own SDK so that
507
- * SDK-level auth policy is honoured.
488
+ * Plugins may use the full public {@link KanbanSDK} contract here, including
489
+ * extension lookup and `getConfigSnapshot()`, instead of relying on older
490
+ * helper-only SDK facades. Plugins should prefer this over constructing their
491
+ * own SDK so that SDK-level auth policy is honoured.
508
492
  */
509
- sdk?: CliPluginSdk;
493
+ sdk?: KanbanSDK;
510
494
  /**
511
495
  * Core-owned CLI auth helper.
512
496
  *
package/src/sdk/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Card, CardFormAttachment, CardFormDataMap, Priority, ResolvedFormDescriptor } from '../shared/types'
2
2
  import type { CapabilitySelections, Webhook } from '../shared/config'
3
+ import type { KanbanSDK } from './KanbanSDK'
3
4
  import type { StorageEngine, StorageEngineType } from './plugins/types'
4
5
  import type { CardStateCursor } from './plugins'
5
6
 
@@ -615,28 +616,11 @@ export class CardStateError extends Error {
615
616
  }
616
617
 
617
618
  /**
618
- * Minimal SDK webhook facade supplied to CLI plugins via {@link CliPluginContext}.
619
- *
620
- * Structural subset of `KanbanSDK`; plugins should use this surface instead of
621
- * importing `KanbanSDK` directly so they remain decoupled from core internals.
619
+ * @deprecated Use {@link KanbanSDK}. CLI plugin hosts now advertise the full
620
+ * public SDK surface, including `getConfigSnapshot()`, instead of a narrowed
621
+ * webhook-only facade.
622
622
  */
623
- export interface CliPluginSdk {
624
- /**
625
- * Returns the SDK extension bag contributed by the plugin with the given id,
626
- * when the host is backed by a full `KanbanSDK` instance.
627
- *
628
- * CLI plugins should prefer this extension path when available and fall back
629
- * to compatibility methods only when running against older or mocked SDK facades.
630
- */
631
- getExtension?<T extends Record<string, unknown> = Record<string, unknown>>(id: string): T | undefined
632
- listWebhooks(): Webhook[]
633
- createWebhook(input: { url: string; events: string[]; secret?: string }): Promise<Webhook>
634
- updateWebhook(
635
- id: string,
636
- updates: Partial<Pick<Webhook, 'url' | 'events' | 'secret' | 'active'>>,
637
- ): Promise<Webhook | null>
638
- deleteWebhook(id: string): Promise<boolean>
639
- }
623
+ export type CliPluginSdk = KanbanSDK
640
624
 
641
625
  /**
642
626
  * Runtime context supplied to a {@link KanbanCliPlugin} when it is invoked by
@@ -650,10 +634,12 @@ export interface CliPluginContext {
650
634
  *
651
635
  * Present when the plugin is invoked through the core `kl` CLI.
652
636
  * Absent in isolated unit tests or standalone invocations.
653
- * Plugins should prefer this over constructing their own SDK so that
654
- * SDK-level auth policy is honoured.
637
+ * Plugins may use the full public {@link KanbanSDK} contract here, including
638
+ * extension lookup and `getConfigSnapshot()`, instead of relying on older
639
+ * helper-only SDK facades. Plugins should prefer this over constructing their
640
+ * own SDK so that SDK-level auth policy is honoured.
655
641
  */
656
- sdk?: CliPluginSdk
642
+ sdk?: KanbanSDK
657
643
  /**
658
644
  * Core-owned CLI auth helper.
659
645
  *
@@ -482,6 +482,12 @@ function loadDotEnv(dir: string): void {
482
482
  * @returns The processed node (same reference for objects/arrays; new primitive for strings).
483
483
  */
484
484
  function resolveConfigEnvVars(node: unknown, configFileName: string, nodePath = ''): unknown {
485
+ const isFormDefaultDataPath = /^\.forms\.(?:[^.]+|"[^"]+")\.data(?:$|[.\[])/.test(nodePath)
486
+
487
+ if (isFormDefaultDataPath) {
488
+ return node
489
+ }
490
+
485
491
  if (typeof node === 'string') {
486
492
  return node.replace(/\$\{([^}]+)\}/g, (_match, varName: string) => {
487
493
  const envValue = process.env[varName]
@@ -350,6 +350,86 @@ describe('Standalone Server Integration', () => {
350
350
  }
351
351
  })
352
352
 
353
+ it('passes the resolved public SDK to standalone plugin registration and request contexts', async () => {
354
+ const cleanup = installTempPackage(
355
+ 'standalone-sdk-context-test-plugin',
356
+ `module.exports = {
357
+ authIdentityPlugin: {
358
+ manifest: { id: 'standalone-sdk-context-test-plugin', provides: ['auth.identity'] },
359
+ async resolveIdentity() {
360
+ return { subject: 'standalone-sdk-context-test-plugin' }
361
+ },
362
+ },
363
+ standaloneHttpPlugin: {
364
+ manifest: { id: 'standalone-sdk-context-test-plugin', provides: ['standalone.http'] },
365
+ registerMiddleware() {
366
+ return [async (request) => {
367
+ if (!request.route('GET', '/api/plugin-sdk-context')) return false
368
+ request.mergeAuthContext({ actorHint: 'middleware-auth-context' })
369
+ return false
370
+ }]
371
+ },
372
+ registerRoutes(options) {
373
+ return [async (request) => {
374
+ if (!request.route('GET', '/api/plugin-sdk-context')) return false
375
+ const registrationSnapshot = options.sdk ? options.sdk.getConfigSnapshot() : null
376
+ const requestSnapshot = request.sdk.getConfigSnapshot()
377
+ const registrationBoard = options.sdk ? options.sdk.getBoard('default') : null
378
+ const requestBoard = request.sdk.getBoard('default')
379
+ request.res.statusCode = 200
380
+ request.res.setHeader('Content-Type', 'application/json')
381
+ request.res.end(JSON.stringify({
382
+ ok: true,
383
+ registrationSdkAvailable: !!options.sdk,
384
+ registrationPort: registrationSnapshot ? registrationSnapshot.port ?? null : null,
385
+ registrationBoardName: registrationBoard ? registrationBoard.name : null,
386
+ requestPort: requestSnapshot.port ?? null,
387
+ requestBoardName: requestBoard.name,
388
+ workspaceRootMatches: options.workspaceRoot === request.workspaceRoot,
389
+ kanbanDirMatches: options.kanbanDir === request.kanbanDir,
390
+ actorHint: request.getAuthContext().actorHint ?? null,
391
+ }))
392
+ return true
393
+ }]
394
+ },
395
+ },
396
+ }
397
+ `,
398
+ )
399
+
400
+ const workspaceRoot = path.dirname(tempDir)
401
+ const resolvedConfigPath = writeWorkspaceConfig(workspaceRoot, {
402
+ port,
403
+ auth: {
404
+ 'auth.identity': { provider: 'standalone-sdk-context-test-plugin' },
405
+ 'auth.policy': { provider: 'noop' },
406
+ },
407
+ })
408
+
409
+ const localPort = await getPort()
410
+ const localServer = startServer(tempDir, localPort, webviewDir, resolvedConfigPath)
411
+ await sleep(200)
412
+
413
+ try {
414
+ const res = await httpGet(`http://localhost:${localPort}/api/plugin-sdk-context`)
415
+ expect(res.status).toBe(200)
416
+ expect(JSON.parse(res.body)).toEqual({
417
+ ok: true,
418
+ registrationSdkAvailable: true,
419
+ registrationPort: port,
420
+ registrationBoardName: 'Default',
421
+ requestPort: port,
422
+ requestBoardName: 'Default',
423
+ workspaceRootMatches: true,
424
+ kanbanDirMatches: true,
425
+ actorHint: 'middleware-auth-context',
426
+ })
427
+ } finally {
428
+ cleanup()
429
+ await new Promise<void>((resolve) => localServer.close(() => resolve()))
430
+ }
431
+ })
432
+
353
433
  it('starts even when the Swagger UI package logo file is unavailable', async () => {
354
434
  vi.resetModules()
355
435
 
@@ -2561,7 +2641,7 @@ describe('Standalone Server Integration', () => {
2561
2641
  const kanbanJson = path.join(path.dirname(tempDir), '.kanban.json')
2562
2642
  fs.writeFileSync(kanbanJson, JSON.stringify({
2563
2643
  version: 2,
2564
- boards: { default: { name: 'Default', columns: [], nextCardId: 1, defaultStatus: 'backlog', defaultPriority: 'medium' } },
2644
+ boards: { default: { name: 'Default', columns: [{ id: 'backlog', name: 'Backlog' }], nextCardId: 1, defaultStatus: 'backlog', defaultPriority: 'medium' } },
2565
2645
  defaultBoard: 'default',
2566
2646
  kanbanDirectory: '.kanban',
2567
2647
  forms: {
@@ -2585,7 +2665,6 @@ describe('Standalone Server Integration', () => {
2585
2665
  }
2586
2666
  }, null, 2), 'utf-8')
2587
2667
 
2588
-
2589
2668
  const createRes = await httpRequest('POST', `http://localhost:${port}/api/tasks`, {
2590
2669
  content: '# Interpolation Regression Card',
2591
2670
  status: 'backlog',
@@ -1,11 +1,21 @@
1
+ import * as http from 'http'
1
2
  import { extractAuthContext, getAuthErrorLike } from '../authUtils'
3
+ import type { AuthContext } from '../../sdk/types'
2
4
  import type { StandaloneContext } from '../context'
3
5
  import { clearClientEditingCard, setClientEditingCard } from '../broadcastService'
4
6
  import { handleMessage } from '../messageHandlers'
5
7
 
6
- export function attachWebSocketHandlers(ctx: StandaloneContext): void {
7
- ctx.wss.on('connection', (ws, req) => {
8
- const authContext = extractAuthContext(req)
8
+ export function attachWebSocketHandlers(
9
+ ctx: StandaloneContext,
10
+ resolveAuthContext?: (req: http.IncomingMessage) => Promise<AuthContext>,
11
+ ): void {
12
+ ctx.wss.on('connection', async (ws, req) => {
13
+ let authContext: AuthContext
14
+ try {
15
+ authContext = resolveAuthContext ? await resolveAuthContext(req) : extractAuthContext(req)
16
+ } catch {
17
+ authContext = extractAuthContext(req)
18
+ }
9
19
  setClientEditingCard(ctx, ws, null)
10
20
  ws.on('message', (data) => {
11
21
  let message: unknown
@@ -13,7 +13,7 @@ import { createStandaloneRuntime, getIndexHtml } from './internal/runtime'
13
13
  import { handleBoardRoutes } from './internal/routes/boards'
14
14
  import { handleSystemRoutes } from './internal/routes/system'
15
15
  import { handleTaskRoutes } from './internal/routes/tasks'
16
- import { getRequestAuthContext, mergeRequestAuthContext, setRequestAuthContext } from './authUtils'
16
+ import { extractAuthContext, getRequestAuthContext, mergeRequestAuthContext, setRequestAuthContext } from './authUtils'
17
17
  import { attachWebSocketHandlers } from './internal/websocket'
18
18
  import { matchRoute, type IncomingMessageWithRawBody } from './httpUtils'
19
19
 
@@ -176,6 +176,7 @@ function collectStandaloneHttpHandlers(
176
176
  ): StandaloneHttpHandler[] {
177
177
  const plugins = ctx.sdk.capabilities?.standaloneHttpPlugins ?? []
178
178
  const registrationOptions = {
179
+ sdk: ctx.sdk,
179
180
  workspaceRoot: ctx.workspaceRoot,
180
181
  kanbanDir: ctx.absoluteKanbanDir,
181
182
  capabilities: ctx.sdk.capabilities?.providers ?? {
@@ -338,7 +339,50 @@ export function startServer(kanbanDir: string, port: number, webviewDir?: string
338
339
  reply.hijack()
339
340
  })
340
341
 
341
- attachWebSocketHandlers(ctx)
342
+ // Resolve auth context for WebSocket upgrade requests by running the middleware
343
+ // pipeline so session cookies set by auth plugins (e.g. kl-auth-plugin) are honoured.
344
+ const resolveWsAuthContext = async (req: http.IncomingMessage) => {
345
+ const silentRes = (() => {
346
+ const r: Record<string, unknown> = {
347
+ writableEnded: false,
348
+ writeHead() { return r },
349
+ setHeader() { return r },
350
+ removeHeader() { /* no-op */ },
351
+ getHeader() { return undefined },
352
+ getHeaders() { return {} },
353
+ end(..._args: unknown[]) { (r as { writableEnded: boolean }).writableEnded = true; return r },
354
+ write() { return false },
355
+ }
356
+ return r as unknown as import('http').ServerResponse
357
+ })()
358
+ const reqWithBody = req as IncomingMessageWithRawBody
359
+ const wsUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
360
+ const requestContext: StandaloneRequestContext = {
361
+ ctx,
362
+ sdk: ctx.sdk,
363
+ workspaceRoot: ctx.workspaceRoot,
364
+ kanbanDir: ctx.absoluteKanbanDir,
365
+ req: reqWithBody,
366
+ res: silentRes,
367
+ url: wsUrl,
368
+ pathname: wsUrl.pathname,
369
+ method: 'GET',
370
+ resolvedWebviewDir,
371
+ indexHtml: resolvedIndexHtml,
372
+ route: createRouteMatcher('GET', wsUrl.pathname, matchRoute),
373
+ isApiRequest: false,
374
+ isPageRequest: false,
375
+ getAuthContext: () => getRequestAuthContext(req),
376
+ setAuthContext: (auth) => setRequestAuthContext(req, auth),
377
+ mergeAuthContext: (auth) => mergeRequestAuthContext(req, auth),
378
+ }
379
+ for (const handler of middlewareHandlers) {
380
+ if (await handler(requestContext)) break
381
+ }
382
+ return extractAuthContext(req)
383
+ }
384
+
385
+ attachWebSocketHandlers(ctx, resolveWsAuthContext)
342
386
  setupStandaloneLifecycle(ctx, fastify.server)
343
387
 
344
388
  const effectiveConfigPath = resolvedConfigPath ?? configPath(path.dirname(ctx.absoluteKanbanDir))
@@ -0,0 +1,5 @@
1
+ {
2
+ "cardId": "1",
3
+ "boardId": "default",
4
+ "updatedAt": "2026-03-26T00:54:58.370Z"
5
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ version: 1
3
+ id: "1"
4
+ status: "deleted"
5
+ priority: "medium"
6
+ assignee: null
7
+ dueDate: null
8
+ created: "2026-03-26T00:54:50.866Z"
9
+ modified: "2026-03-26T00:55:04.504Z"
10
+ completedAt: null
11
+ labels: []
12
+ attachments:
13
+ - "1.log"
14
+ order: "a0"
15
+ ---
16
+
17
+ # dddd
@@ -0,0 +1 @@
1
+ 2026-03-26T00:55:04.503Z [system] Status changed: `in-progress` → `deleted` {"previousStatus":"in-progress","status":"deleted","activity":{"type":"card.status.changed","qualifiesForUnread":true}}
@@ -0,0 +1,59 @@
1
+ {
2
+ "version": 2,
3
+ "boards": {
4
+ "default": {
5
+ "name": "Default",
6
+ "columns": [
7
+ {
8
+ "id": "backlog",
9
+ "name": "Backlog",
10
+ "color": "#6b7280"
11
+ },
12
+ {
13
+ "id": "todo",
14
+ "name": "To Do",
15
+ "color": "#3b82f6"
16
+ },
17
+ {
18
+ "id": "in-progress",
19
+ "name": "In Progress",
20
+ "color": "#f59e0b"
21
+ },
22
+ {
23
+ "id": "review",
24
+ "name": "Review",
25
+ "color": "#8b5cf6"
26
+ },
27
+ {
28
+ "id": "done",
29
+ "name": "Done",
30
+ "color": "#22c55e"
31
+ }
32
+ ],
33
+ "nextCardId": 1,
34
+ "defaultStatus": "backlog",
35
+ "defaultPriority": "medium"
36
+ }
37
+ },
38
+ "defaultBoard": "default",
39
+ "kanbanDirectory": ".kanban",
40
+ "aiAgent": "claude",
41
+ "defaultPriority": "medium",
42
+ "defaultStatus": "backlog",
43
+ "nextCardId": 2,
44
+ "showPriorityBadges": true,
45
+ "showAssignee": true,
46
+ "showDueDate": true,
47
+ "showLabels": true,
48
+ "showBuildWithAI": true,
49
+ "showFileName": false,
50
+ "compactMode": false,
51
+ "markdownEditorMode": false,
52
+ "showDeletedColumn": false,
53
+ "boardZoom": 100,
54
+ "cardZoom": 100,
55
+ "boardBackgroundMode": "fancy",
56
+ "boardBackgroundPreset": "aurora",
57
+ "port": 2954,
58
+ "labels": {}
59
+ }