@ensera/plugin-backend 1.2.0 → 1.2.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/dist/index.d.ts CHANGED
@@ -1,5 +1,51 @@
1
1
  import { Request, Response, NextFunction, Express } from 'express';
2
2
 
3
+ type CoarseCapabilities = {
4
+ /** Can invite / remove workspace members. OWNER/ADMIN only. */
5
+ canInviteMembers: boolean;
6
+ /** Can create, rename, delete, or reorder spaces. OWNER/ADMIN only. */
7
+ canManageSpaces: boolean;
8
+ /** Can install or uninstall workspace-level features. OWNER/ADMIN only. */
9
+ canInstallWorkspaceFeatures: boolean;
10
+ /** Can create feature instances inside spaces. OWNER/ADMIN only. */
11
+ canCreateFeatureInstances: boolean;
12
+ /**
13
+ * Can create new content items (tasks, cards, rows, etc.) inside a feature.
14
+ * False for MEMBER role — they may edit assigned content but cannot create.
15
+ * True for OWNER/ADMIN and for non-MEMBER actors with EDIT+ permission.
16
+ */
17
+ canCreateContent: boolean;
18
+ /**
19
+ * Can delete any content item regardless of ownership.
20
+ * True for OWNER/ADMIN or actors with FULL feature permission.
21
+ */
22
+ canDeleteAnyContent: boolean;
23
+ /**
24
+ * Can delete content items the actor personally created.
25
+ * False for MEMBER role. True for non-MEMBER actors with EDIT+ permission.
26
+ */
27
+ canDeleteOwnContent: boolean;
28
+ /**
29
+ * Can assign content items (tasks, cards) to other workspace members.
30
+ * OWNER/ADMIN only.
31
+ */
32
+ canAssignContent: boolean;
33
+ /**
34
+ * Can manage structural elements (columns, sections, board layout).
35
+ * OWNER/ADMIN or actors with FULL feature permission.
36
+ */
37
+ canManageContentStructure: boolean;
38
+ };
39
+ type CoarseAuthorization = {
40
+ workspaceRole: "OWNER" | "ADMIN" | "MEMBER" | "GUEST" | null;
41
+ /** Effective space-level permission (FULL|EDIT|COMMENT|VIEW|NO_ACCESS). */
42
+ spacePermission: string;
43
+ /** Effective feature-level permission (FULL|EDIT|COMMENT|VIEW|NO_ACCESS). */
44
+ featurePermission: string;
45
+ accessSource: "workspace" | "space" | "feature" | "none";
46
+ capabilities: CoarseCapabilities;
47
+ policyVersion: "v2";
48
+ };
3
49
  /**
4
50
  * Plugin scope extracted from JWT token
5
51
  */
@@ -11,6 +57,11 @@ type PluginScope = {
11
57
  featureSlug: string;
12
58
  tabViewId?: string;
13
59
  taskScopeId?: string;
60
+ /**
61
+ * Coarse authorization resolved by core at token-mint time.
62
+ * Use this for permission checks in route handlers and command handlers.
63
+ */
64
+ authorization?: CoarseAuthorization;
14
65
  roles?: string[];
15
66
  perms?: string[];
16
67
  tokenId?: string;
@@ -86,9 +137,12 @@ interface NotificationAction {
86
137
  */
87
138
  interface BackendNotification {
88
139
  userId: string;
140
+ actorId?: string | null;
89
141
  type: string;
90
142
  title: string;
91
143
  message: string;
144
+ entityType?: string;
145
+ entityId?: string;
92
146
  metadata?: JsonValue;
93
147
  priority?: "low" | "normal" | "high" | "urgent";
94
148
  link?: string;
@@ -114,6 +168,12 @@ interface NotificationPublishResponse {
114
168
  sent?: boolean;
115
169
  emailSent?: boolean;
116
170
  pushSent?: boolean;
171
+ scheduled?: boolean;
172
+ scheduledFor?: string;
173
+ warnings?: string[];
174
+ errorCode?: string;
175
+ hint?: string;
176
+ nextSteps?: string[];
117
177
  error?: string;
118
178
  reason?: string;
119
179
  }
@@ -135,6 +195,9 @@ interface NotificationBulkResponse {
135
195
  interface NotificationCancelResponse {
136
196
  success: boolean;
137
197
  cancelled: number;
198
+ errorCode?: string;
199
+ hint?: string;
200
+ nextSteps?: string[];
138
201
  error?: string;
139
202
  }
140
203
  /**
@@ -163,6 +226,8 @@ interface NotificationPublisherConfig {
163
226
  coreApiUrl: string;
164
227
  /** Service-to-service authentication token */
165
228
  serviceToken: string;
229
+ /** Developer Notification Token for Core-side notification authorization */
230
+ notificationToken?: string;
166
231
  /** Timeout in ms (default 5000) */
167
232
  timeoutMs?: number;
168
233
  }
@@ -381,6 +446,29 @@ type CoreRequestContext = {
381
446
  userId: string;
382
447
  taskScopeId?: string;
383
448
  requestId: string;
449
+ /**
450
+ * Structured coarse authorization resolved by core for this feature instance.
451
+ * Prefer this over the flat legacy fields below for new code.
452
+ */
453
+ authorization?: CoarseAuthorization;
454
+ /**
455
+ * Effective feature-level permission string.
456
+ * Kept for backward compatibility; use authorization.featurePermission instead.
457
+ * Values: "FULL" | "EDIT" | "COMMENT" | "VIEW" | "NO_ACCESS"
458
+ */
459
+ effectiveRole?: string;
460
+ /**
461
+ * Optional feature ownership/publisher attribution resolved by Core.
462
+ * Use this for logs, support diagnostics, or future ownership-aware behavior.
463
+ */
464
+ developer?: {
465
+ ownerUserId?: string | null;
466
+ ownerDeveloperProfileId?: string | null;
467
+ ownerDisplayName?: string | null;
468
+ ownerDeveloperType?: string | null;
469
+ releaseId?: string | null;
470
+ storageMode?: string | null;
471
+ };
384
472
  };
385
473
  type CommandHandler = (input: Record<string, unknown>, context: CoreRequestContext) => Promise<unknown>;
386
474
  type CommandRegistry = {
@@ -395,6 +483,7 @@ type CreateDbOptions = {
395
483
  prismaDir?: string;
396
484
  coreApiUrl?: string;
397
485
  serviceToken?: string;
486
+ managedDbSessionToken?: string;
398
487
  runMigrations?: boolean;
399
488
  };
400
489
  type DbProvisionResult = {
@@ -404,4 +493,4 @@ type DbProvisionResult = {
404
493
  };
405
494
  declare function provisionDb(options?: CreateDbOptions): Promise<DbProvisionResult>;
406
495
 
407
- export { type BackendNotification, type CommandHandler, type CommandRegistry, type CoreRequestContext, type CreateDbOptions, type DbProvisionResult, type JsonObject, type JsonValue, type NotificationAction, type NotificationApiPayload, type NotificationBulkApiPayload, type NotificationBulkResponse, type NotificationCancelResponse, type NotificationCapabilities, type NotificationOptions, type NotificationPublishResponse, type NotificationPublisher, type NotificationPublisherConfig, type NotificationTypeConfig, type PluginAuthOptions, type PluginContext, PluginError, type PluginErrorResponse, type PluginScope, type RequestWithContext, type ScheduleOptions, assertPluginScope, buildDateTime, calculateScheduleTime, createCommandRegistry, createCoreRouter, createNotificationPublisher, forbidClientInstanceId, formatTime, getPluginScope, isPastDate, pluginAuth, pluginErrorHandler, provisionDb, requireFeature, requirePermission, requireTabView, requireTaskScope, resolveEffectiveInstanceId, toDateString, withCoreServiceToken, withInstanceOnly, withInstanceScope, withUserAndInstance };
496
+ export { type BackendNotification, type CoarseAuthorization, type CoarseCapabilities, type CommandHandler, type CommandRegistry, type CoreRequestContext, type CreateDbOptions, type DbProvisionResult, type JsonObject, type JsonValue, type NotificationAction, type NotificationApiPayload, type NotificationBulkApiPayload, type NotificationBulkResponse, type NotificationCancelResponse, type NotificationCapabilities, type NotificationOptions, type NotificationPublishResponse, type NotificationPublisher, type NotificationPublisherConfig, type NotificationTypeConfig, type PluginAuthOptions, type PluginContext, PluginError, type PluginErrorResponse, type PluginScope, type RequestWithContext, type ScheduleOptions, assertPluginScope, buildDateTime, calculateScheduleTime, createCommandRegistry, createCoreRouter, createNotificationPublisher, forbidClientInstanceId, formatTime, getPluginScope, isPastDate, pluginAuth, pluginErrorHandler, provisionDb, requireFeature, requirePermission, requireTabView, requireTaskScope, resolveEffectiveInstanceId, toDateString, withCoreServiceToken, withInstanceOnly, withInstanceScope, withUserAndInstance };
package/dist/index.js CHANGED
@@ -32,6 +32,14 @@ function pickClaim(obj, key) {
32
32
  const v = obj?.[key];
33
33
  return typeof v === "string" && v.length > 0 ? v : void 0;
34
34
  }
35
+ function isCoarseAuthorization(value) {
36
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37
+ return false;
38
+ }
39
+ const candidate = value;
40
+ const capabilities = candidate.capabilities;
41
+ return (candidate.workspaceRole === null || typeof candidate.workspaceRole === "string") && typeof candidate.spacePermission === "string" && typeof candidate.featurePermission === "string" && typeof candidate.accessSource === "string" && !!capabilities && typeof capabilities === "object" && !Array.isArray(capabilities) && typeof capabilities.canInviteMembers === "boolean" && typeof capabilities.canManageSpaces === "boolean" && typeof capabilities.canInstallWorkspaceFeatures === "boolean" && typeof capabilities.canCreateFeatureInstances === "boolean" && candidate.policyVersion === "v2";
42
+ }
35
43
  function normalizeScope(payload) {
36
44
  const userId = pickClaim(payload, "sub");
37
45
  const workspaceId = pickClaim(payload, "workspaceId");
@@ -55,6 +63,7 @@ function normalizeScope(payload) {
55
63
  featureSlug,
56
64
  tabViewId,
57
65
  taskScopeId,
66
+ authorization: isCoarseAuthorization(payload.authorization) ? payload.authorization : void 0,
58
67
  roles: Array.isArray(payload.roles) ? payload.roles : void 0,
59
68
  perms: Array.isArray(payload.perms) ? payload.perms : void 0,
60
69
  tokenId: pickClaim(payload, "jti"),
@@ -298,18 +307,101 @@ function forbidClientInstanceId(options = {}) {
298
307
  }
299
308
 
300
309
  // src/notify.ts
310
+ function isLoopbackHost(hostname) {
311
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "::ffff:127.0.0.1" || hostname === "0.0.0.0" || hostname === "host.docker.internal";
312
+ }
313
+ function normalizeScheduledOptions(options) {
314
+ if (!options) {
315
+ return void 0;
316
+ }
317
+ return {
318
+ ...options,
319
+ scheduledAt: options.scheduledAt instanceof Date ? options.scheduledAt.toISOString() : options.scheduledAt
320
+ };
321
+ }
301
322
  function createNotificationPublisher(config) {
302
323
  const { featureSlug, coreApiUrl, serviceToken, timeoutMs = 5e3 } = config;
303
324
  if (!featureSlug?.trim()) {
304
- throw new Error("NotificationPublisher: featureSlug is required");
325
+ throw new Error(
326
+ "NotificationPublisher: featureSlug is required for Core-attributed notification sends."
327
+ );
305
328
  }
306
329
  if (!coreApiUrl?.trim()) {
307
- throw new Error("NotificationPublisher: coreApiUrl is required");
330
+ throw new Error(
331
+ "NotificationPublisher: coreApiUrl is required. Set CORE_API_URL before sending notifications."
332
+ );
308
333
  }
309
334
  if (!serviceToken?.trim()) {
310
- throw new Error("NotificationPublisher: serviceToken is required");
335
+ throw new Error(
336
+ "NotificationPublisher: serviceToken is required. Set CORE_SERVICE_TOKEN or SERVICE_TOKEN from the installation Developer Settings page."
337
+ );
338
+ }
339
+ function getNotificationToken() {
340
+ return config.notificationToken?.trim() || process.env.ENSERA_NOTIFICATION_TOKEN?.trim() || process.env.NOTIFICATION_TOKEN?.trim() || process.env.ENSERA_DEVELOPER_NOTIFICATION_TOKEN?.trim() || void 0;
341
+ }
342
+ function getCoreApiUrls() {
343
+ const normalized = coreApiUrl.replace(/\/+$/, "");
344
+ const candidates = [normalized];
345
+ try {
346
+ const fallbackUrl = new URL(normalized);
347
+ if (fallbackUrl.hostname === "host.docker.internal") {
348
+ fallbackUrl.hostname = "localhost";
349
+ candidates.push(fallbackUrl.toString().replace(/\/+$/, ""));
350
+ }
351
+ } catch {
352
+ }
353
+ return [...new Set(candidates)];
354
+ }
355
+ function allowsNotificationCompatibilityBridge() {
356
+ if ((process.env.ENSERA_ALLOW_LEGACY_NOTIFICATION_ACCESS ?? "").trim() === "1") {
357
+ return true;
358
+ }
359
+ if (process.env.NODE_ENV === "production") {
360
+ return false;
361
+ }
362
+ try {
363
+ return isLoopbackHost(new URL(coreApiUrl).hostname);
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
368
+ function buildMissingNotificationTokenError() {
369
+ return `[NOTIFICATION_TOKEN_REQUIRED] Backend notifications for '${featureSlug}' require a Notification Token. Open Developer Settings > Tokens, reveal or rotate the Notification Token, set ENSERA_NOTIFICATION_TOKEN or NOTIFICATION_TOKEN in the backend environment, and retry.`;
370
+ }
371
+ function getNotificationPreflightError() {
372
+ if (getNotificationToken() || allowsNotificationCompatibilityBridge()) {
373
+ return null;
374
+ }
375
+ return buildMissingNotificationTokenError();
376
+ }
377
+ function formatCoreError2(status, data) {
378
+ const code = typeof data?.errorCode === "string" && data.errorCode.trim() ? `[${data.errorCode.trim()}] ` : "";
379
+ const message = typeof data?.error === "string" && data.error.trim() ? data.error.trim() : `HTTP ${status}`;
380
+ const hints = [
381
+ typeof data?.hint === "string" ? data.hint.trim() : void 0,
382
+ ...Array.isArray(data?.nextSteps) ? data.nextSteps.filter(
383
+ (entry) => typeof entry === "string" && entry.trim().length > 0
384
+ ) : []
385
+ ];
386
+ return hints.length > 0 ? `${code}${message} ${hints.join(" ")}` : `${code}${message}`;
387
+ }
388
+ function buildHeaders(notificationToken) {
389
+ return {
390
+ "Content-Type": "application/json",
391
+ Authorization: `Bearer ${serviceToken}`,
392
+ "X-Feature-Slug": featureSlug,
393
+ ...notificationToken ? {
394
+ "X-Ensera-Notification-Token": notificationToken
395
+ } : {}
396
+ };
397
+ }
398
+ async function readJson(response) {
399
+ try {
400
+ return await response.json();
401
+ } catch {
402
+ return null;
403
+ }
311
404
  }
312
- const baseUrl = coreApiUrl.replace(/\/+$/, "");
313
405
  async function fetchWithTimeout(url, options) {
314
406
  const controller = new AbortController();
315
407
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -328,52 +420,84 @@ function createNotificationPublisher(config) {
328
420
  throw error;
329
421
  }
330
422
  }
423
+ async function requestCore(path2, init) {
424
+ let lastError = null;
425
+ const notificationToken = getNotificationToken();
426
+ for (const baseUrl of getCoreApiUrls()) {
427
+ try {
428
+ return await fetchWithTimeout(`${baseUrl}${path2}`, {
429
+ ...init,
430
+ headers: {
431
+ ...buildHeaders(notificationToken),
432
+ ...init.headers ?? {}
433
+ }
434
+ });
435
+ } catch (error) {
436
+ lastError = error;
437
+ }
438
+ }
439
+ throw lastError instanceof Error ? lastError : new Error("Failed to reach Core notification API");
440
+ }
441
+ function toPublishFailure(status, data) {
442
+ return {
443
+ success: false,
444
+ error: formatCoreError2(status, data),
445
+ errorCode: typeof data?.errorCode === "string" ? data.errorCode : void 0,
446
+ hint: typeof data?.hint === "string" ? data.hint : void 0,
447
+ nextSteps: Array.isArray(data?.nextSteps) ? data.nextSteps.filter(
448
+ (entry) => typeof entry === "string" && entry.trim().length > 0
449
+ ) : void 0
450
+ };
451
+ }
452
+ function toCancelFailure(status, data) {
453
+ return {
454
+ success: false,
455
+ cancelled: 0,
456
+ error: formatCoreError2(status, data),
457
+ errorCode: typeof data?.errorCode === "string" ? data.errorCode : void 0,
458
+ hint: typeof data?.hint === "string" ? data.hint : void 0,
459
+ nextSteps: Array.isArray(data?.nextSteps) ? data.nextSteps.filter(
460
+ (entry) => typeof entry === "string" && entry.trim().length > 0
461
+ ) : void 0
462
+ };
463
+ }
331
464
  async function publish(notification, options) {
332
- const url = `${baseUrl}/api/notifications/publish`;
333
- const processedOptions = options ? {
334
- ...options,
335
- scheduledAt: options.scheduledAt instanceof Date ? options.scheduledAt.toISOString() : options.scheduledAt
336
- } : void 0;
465
+ const preflightError = getNotificationPreflightError();
466
+ if (preflightError) {
467
+ return {
468
+ success: false,
469
+ error: preflightError,
470
+ errorCode: "NOTIFICATION_TOKEN_REQUIRED"
471
+ };
472
+ }
337
473
  const payload = {
338
474
  ...notification,
339
475
  featureSlug,
340
- options: processedOptions
476
+ options: normalizeScheduledOptions(options)
341
477
  };
342
- console.log("[NotificationPublisher] Publishing notification:", {
343
- featureSlug,
344
- userId: notification.userId,
345
- type: notification.type,
346
- scheduledAt: processedOptions?.scheduledAt,
347
- hasScheduledAt: !!processedOptions?.scheduledAt
348
- });
349
478
  try {
350
- const response = await fetchWithTimeout(url, {
479
+ const response = await requestCore("/api/notifications/publish", {
351
480
  method: "POST",
352
- headers: {
353
- "Content-Type": "application/json",
354
- Authorization: `Bearer ${serviceToken}`,
355
- "X-Feature-Slug": featureSlug
356
- },
357
481
  body: JSON.stringify(payload)
358
482
  });
359
- const data = await response.json();
360
- console.log("[NotificationPublisher] Response:", {
361
- ok: response.ok,
362
- status: response.status,
363
- success: data.success,
364
- scheduled: data.scheduled,
365
- scheduledFor: data.scheduledFor,
366
- error: data.error
367
- });
483
+ const data = await readJson(response);
368
484
  if (!response.ok) {
369
- return {
370
- success: false,
371
- error: data.error || `HTTP ${response.status}`
372
- };
485
+ return toPublishFailure(response.status, data);
373
486
  }
374
- return data;
487
+ return {
488
+ success: true,
489
+ id: typeof data?.id === "string" ? data.id : void 0,
490
+ sent: typeof data?.sent === "boolean" ? data.sent : void 0,
491
+ emailSent: typeof data?.emailSent === "boolean" ? data.emailSent : void 0,
492
+ pushSent: typeof data?.pushSent === "boolean" ? data.pushSent : void 0,
493
+ scheduled: typeof data?.scheduled === "boolean" ? data.scheduled : void 0,
494
+ scheduledFor: typeof data?.scheduledFor === "string" ? data.scheduledFor : void 0,
495
+ reason: typeof data?.reason === "string" ? data.reason : void 0,
496
+ warnings: Array.isArray(data?.warnings) ? data.warnings.filter(
497
+ (entry) => typeof entry === "string" && entry.trim().length > 0
498
+ ) : void 0
499
+ };
375
500
  } catch (error) {
376
- console.error("[NotificationPublisher] Error:", error);
377
501
  return {
378
502
  success: false,
379
503
  error: error.message || "Failed to publish notification"
@@ -381,23 +505,31 @@ function createNotificationPublisher(config) {
381
505
  }
382
506
  }
383
507
  async function publishBulk(notifications, options) {
384
- const url = `${baseUrl}/api/notifications/bulk`;
508
+ const preflightError = getNotificationPreflightError();
509
+ if (preflightError) {
510
+ return {
511
+ success: false,
512
+ count: 0,
513
+ errors: [
514
+ {
515
+ index: 0,
516
+ userId: "",
517
+ error: preflightError
518
+ }
519
+ ]
520
+ };
521
+ }
385
522
  const payload = {
386
523
  notifications,
387
524
  featureSlug,
388
- options
525
+ options: normalizeScheduledOptions(options)
389
526
  };
390
527
  try {
391
- const response = await fetchWithTimeout(url, {
528
+ const response = await requestCore("/api/notifications/bulk", {
392
529
  method: "POST",
393
- headers: {
394
- "Content-Type": "application/json",
395
- Authorization: `Bearer ${serviceToken}`,
396
- "X-Feature-Slug": featureSlug
397
- },
398
530
  body: JSON.stringify(payload)
399
531
  });
400
- const data = await response.json();
532
+ const data = await readJson(response);
401
533
  if (!response.ok) {
402
534
  return {
403
535
  success: false,
@@ -406,12 +538,16 @@ function createNotificationPublisher(config) {
406
538
  {
407
539
  index: 0,
408
540
  userId: "",
409
- error: data.error || `HTTP ${response.status}`
541
+ error: formatCoreError2(response.status, data)
410
542
  }
411
543
  ]
412
544
  };
413
545
  }
414
- return data;
546
+ return {
547
+ success: Boolean(data?.success),
548
+ count: typeof data?.count === "number" ? data.count : 0,
549
+ errors: Array.isArray(data?.errors) ? data.errors : void 0
550
+ };
415
551
  } catch (error) {
416
552
  return {
417
553
  success: false,
@@ -434,46 +570,32 @@ function createNotificationPublisher(config) {
434
570
  });
435
571
  }
436
572
  async function cancel(metadata) {
437
- const url = `${baseUrl}/api/notifications/cancel-scheduled`;
438
- const payload = {
439
- featureSlug,
440
- metadata
441
- };
442
- console.log("[NotificationPublisher] Cancelling notifications:", {
443
- featureSlug,
444
- metadata
445
- });
573
+ const preflightError = getNotificationPreflightError();
574
+ if (preflightError) {
575
+ return {
576
+ success: false,
577
+ cancelled: 0,
578
+ error: preflightError,
579
+ errorCode: "NOTIFICATION_TOKEN_REQUIRED"
580
+ };
581
+ }
446
582
  try {
447
- const response = await fetchWithTimeout(url, {
583
+ const response = await requestCore("/api/notifications/cancel-scheduled", {
448
584
  method: "POST",
449
- headers: {
450
- "Content-Type": "application/json",
451
- Authorization: `Bearer ${serviceToken}`,
452
- "X-Feature-Slug": featureSlug
453
- },
454
- body: JSON.stringify(payload)
455
- });
456
- const data = await response.json();
457
- console.log("[NotificationPublisher] Cancel response:", {
458
- ok: response.ok,
459
- status: response.status,
460
- success: data.success,
461
- cancelled: data.cancelled,
462
- error: data.error
585
+ body: JSON.stringify({
586
+ featureSlug,
587
+ metadata
588
+ })
463
589
  });
590
+ const data = await readJson(response);
464
591
  if (!response.ok) {
465
- return {
466
- success: false,
467
- cancelled: 0,
468
- error: data.error || `HTTP ${response.status}`
469
- };
592
+ return toCancelFailure(response.status, data);
470
593
  }
471
594
  return {
472
595
  success: true,
473
- cancelled: data.cancelled || 0
596
+ cancelled: typeof data?.cancelled === "number" ? data.cancelled : 0
474
597
  };
475
598
  } catch (error) {
476
- console.error("[NotificationPublisher] Cancel error:", error);
477
599
  return {
478
600
  success: false,
479
601
  cancelled: 0,
@@ -764,6 +886,24 @@ function getString(value) {
764
886
  const trimmed = value.trim();
765
887
  return trimmed.length > 0 ? trimmed : void 0;
766
888
  }
889
+ function formatCoreError(status, payload) {
890
+ if (!isRecord(payload)) {
891
+ return `Core returned ${status}. Unknown error`;
892
+ }
893
+ const parts = [
894
+ getString(payload.errorCode) ? `[${getString(payload.errorCode)}]` : void 0,
895
+ getString(payload.error) ?? `Core returned ${status}. Unknown error`
896
+ ].filter(
897
+ (entry) => typeof entry === "string" && entry.trim().length > 0
898
+ );
899
+ const hints = [
900
+ getString(payload.hint),
901
+ ...Array.isArray(payload.nextSteps) ? payload.nextSteps.filter(
902
+ (entry) => typeof entry === "string" && entry.trim().length > 0
903
+ ) : []
904
+ ];
905
+ return hints.length > 0 ? `${parts.join(" ")} ${hints.join(" ")}` : parts.join(" ");
906
+ }
767
907
  function parseProvisionResult(value) {
768
908
  if (!isRecord(value)) {
769
909
  throw new Error("provisionDb: Core returned an invalid response body");
@@ -789,21 +929,26 @@ function parseProvisionResult(value) {
789
929
  async function provisionDb(options = {}) {
790
930
  const coreApiUrl = (options.coreApiUrl ?? process.env.CORE_API_URL)?.replace(/\/+$/, "");
791
931
  const serviceToken = options.serviceToken ?? process.env.CORE_SERVICE_TOKEN;
932
+ const resolvedServiceToken = serviceToken ?? process.env.SERVICE_TOKEN;
933
+ const managedDbSessionToken = options.managedDbSessionToken ?? process.env.ENSERA_MANAGED_DB_SESSION_TOKEN ?? process.env.MANAGED_DB_SESSION_TOKEN ?? process.env.ENSERA_DEVELOPER_DB_TOKEN;
792
934
  if (!coreApiUrl) {
793
935
  throw new Error(
794
936
  "provisionDb: CORE_API_URL is not set. Add it to your .env file."
795
937
  );
796
938
  }
797
- if (!serviceToken) {
939
+ if (!resolvedServiceToken) {
798
940
  throw new Error(
799
- "provisionDb: CORE_SERVICE_TOKEN is not set. Add it to your .env file."
941
+ "provisionDb: CORE_SERVICE_TOKEN or SERVICE_TOKEN is not set. Add it to your .env file."
800
942
  );
801
943
  }
802
944
  const response = await fetch(`${coreApiUrl}/api/internal/db/provision`, {
803
945
  method: "POST",
804
946
  headers: {
805
947
  "Content-Type": "application/json",
806
- Authorization: `Bearer ${serviceToken}`
948
+ Authorization: `Bearer ${resolvedServiceToken}`,
949
+ ...managedDbSessionToken ? {
950
+ "x-ensera-managed-db-session-token": managedDbSessionToken
951
+ } : {}
807
952
  }
808
953
  });
809
954
  let payload = null;
@@ -813,9 +958,8 @@ async function provisionDb(options = {}) {
813
958
  payload = null;
814
959
  }
815
960
  if (!response.ok) {
816
- const errorMessage = isRecord(payload) ? getString(payload.error) : void 0;
817
961
  throw new Error(
818
- `provisionDb: Core returned ${response.status}. ${errorMessage ?? "Unknown error"}`
962
+ `provisionDb: ${formatCoreError(response.status, payload)}`
819
963
  );
820
964
  }
821
965
  const result = parseProvisionResult(payload);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ensera/plugin-backend",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Runtime backend SDK for Ensera plugins.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",