@camstack/addon-remote-storage 1.0.3 → 1.0.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.
package/dist/s3.addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-B4p10WFZ.js");
5
+ const require_shared = require("./shared-uppUFqZL.js");
6
6
  let node_stream = require("node:stream");
7
7
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
8
8
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
package/dist/s3.addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as storageProviderCapability, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as BaseAddon, t as createSessionId } from "./shared-DL9Prz1T.mjs";
1
+ import { c as storageProviderCapability, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as BaseAddon, t as createSessionId } from "./shared-uH6t_fje.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { DeleteObjectCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
4
  import { Upload } from "@aws-sdk/lib-storage";
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-B4p10WFZ.js");
5
+ const require_shared = require("./shared-uppUFqZL.js");
6
6
  let ssh2 = require("ssh2");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_shared.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as storageProviderCapability, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as BaseAddon, t as createSessionId } from "./shared-DL9Prz1T.mjs";
1
+ import { a as safeJoinRemotePath, c as storageProviderCapability, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as BaseAddon, t as createSessionId } from "./shared-uH6t_fje.mjs";
2
2
  import { Client } from "ssh2";
3
3
  import * as path from "node:path";
4
4
  //#region src/providers/sftp/sftp-config-schema.ts
@@ -5132,14 +5132,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
5132
5132
  EventCategory["DeviceSleeping"] = "device.sleeping";
5133
5133
  EventCategory["RetentionCleanup"] = "retention.cleanup";
5134
5134
  /**
5135
- * Progress snapshot emitted by `BulkUpdateCoordinator` on every state
5136
- * transition (item status change, phase change, completion, cancel).
5137
- * Payload is `BulkUpdateState`. Admin UI subscribes via `useLiveEvent`
5138
- * to drive the sticky `BulkUpdateBanner` and per-row `AddonRowBadge`.
5139
- *
5140
- * Spec: docs/superpowers/specs/2026-05-21-addons-bulk-update-progress-design.md
5135
+ * Legacy bulk-update progress snapshot (payload `BulkUpdateState`). No longer
5136
+ * emitted F3 removed the coordinator that produced it; "Update all" now runs
5137
+ * as one lifecycle engine job (`AddonsJobProgress`/`AddonsJobLog`). Retained
5138
+ * (with `BulkUpdateState`) only to avoid regenerating the event maps; removed
5139
+ * in F4 once live bulk progress is re-implemented over the engine events.
5141
5140
  */
5142
5141
  EventCategory["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
5142
+ EventCategory["AddonsJobProgress"] = "addons.job-progress";
5143
+ EventCategory["AddonsJobLog"] = "addons.job-log";
5143
5144
  /**
5144
5145
  * A container's child visibility toggled (hidden/shown). Emitted by the
5145
5146
  * `accessories` cap when a child device is hidden or revealed.
@@ -16004,6 +16005,69 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
16004
16005
  kind: "mutation",
16005
16006
  auth: "admin"
16006
16007
  });
16008
+ var jobKindSchema = _enum([
16009
+ "install",
16010
+ "update",
16011
+ "uninstall",
16012
+ "restart"
16013
+ ]);
16014
+ var taskPhaseSchema = _enum([
16015
+ "queued",
16016
+ "fetching",
16017
+ "staged",
16018
+ "validating",
16019
+ "applying",
16020
+ "restarting",
16021
+ "applied",
16022
+ "done",
16023
+ "failed",
16024
+ "skipped"
16025
+ ]);
16026
+ var taskTargetSchema = _enum(["framework", "addon"]);
16027
+ var taskLogEntrySchema = object({
16028
+ tsMs: number(),
16029
+ nodeId: string(),
16030
+ packageName: string(),
16031
+ phase: taskPhaseSchema,
16032
+ message: string()
16033
+ });
16034
+ var lifecycleTaskSchema = object({
16035
+ taskId: string(),
16036
+ nodeId: string(),
16037
+ packageName: string(),
16038
+ fromVersion: string().nullable(),
16039
+ toVersion: string(),
16040
+ target: taskTargetSchema,
16041
+ phase: taskPhaseSchema,
16042
+ stagedPath: string().nullable(),
16043
+ attempts: number(),
16044
+ steps: array(taskLogEntrySchema),
16045
+ error: string().nullable(),
16046
+ startedAtMs: number().nullable(),
16047
+ finishedAtMs: number().nullable()
16048
+ });
16049
+ var lifecycleJobStateSchema = _enum([
16050
+ "running",
16051
+ "completed",
16052
+ "failed",
16053
+ "partially-failed",
16054
+ "cancelled"
16055
+ ]);
16056
+ var lifecycleJobScopeSchema = _enum([
16057
+ "single",
16058
+ "bulk",
16059
+ "cluster"
16060
+ ]);
16061
+ var lifecycleJobSchema = object({
16062
+ jobId: string(),
16063
+ kind: jobKindSchema,
16064
+ createdAtMs: number(),
16065
+ createdBy: string(),
16066
+ scope: lifecycleJobScopeSchema,
16067
+ tasks: array(lifecycleTaskSchema),
16068
+ state: lifecycleJobStateSchema,
16069
+ schemaVersion: literal(1)
16070
+ });
16007
16071
  /**
16008
16072
  * addons — system-scoped singleton capability for addon package
16009
16073
  * management (install, update, configure, restart) and per-addon log
@@ -16186,7 +16250,7 @@ var BulkUpdatePhaseSchema = _enum([
16186
16250
  "restarting",
16187
16251
  "finalizing"
16188
16252
  ]);
16189
- var BulkUpdateStateSchema = object({
16253
+ object({
16190
16254
  id: string(),
16191
16255
  nodeId: string(),
16192
16256
  startedAtMs: number(),
@@ -16289,20 +16353,7 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
16289
16353
  }), UpdateFrameworkPackageResultSchema, {
16290
16354
  kind: "mutation",
16291
16355
  auth: "admin"
16292
- }), method(object({
16293
- nodeId: string(),
16294
- items: array(object({
16295
- name: string(),
16296
- version: string(),
16297
- isSystem: boolean()
16298
- })).readonly()
16299
- }), object({ id: string() }), {
16300
- kind: "mutation",
16301
- auth: "admin"
16302
- }), method(object({ id: string() }), BulkUpdateStateSchema.nullable(), { auth: "admin" }), method(object({ id: string() }), object({ cancelled: boolean() }), {
16303
- kind: "mutation",
16304
- auth: "admin"
16305
- }), method(object({ nodeId: string().optional() }), array(BulkUpdateStateSchema).readonly(), { auth: "admin" }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
16356
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
16306
16357
  kind: "mutation",
16307
16358
  auth: "admin"
16308
16359
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -16324,6 +16375,24 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
16324
16375
  kind: "mutation",
16325
16376
  auth: "admin"
16326
16377
  }), method(CustomActionInputSchema, unknown(), { kind: "mutation" }), method(object({
16378
+ kind: _enum([
16379
+ "install",
16380
+ "update",
16381
+ "uninstall",
16382
+ "restart"
16383
+ ]),
16384
+ targets: array(object({
16385
+ name: string().min(1),
16386
+ version: string().min(1)
16387
+ })).min(1),
16388
+ nodeIds: array(string()).optional()
16389
+ }), object({ jobId: string() }), {
16390
+ kind: "mutation",
16391
+ auth: "admin"
16392
+ }), method(object({ jobId: string() }), lifecycleJobSchema.nullable(), { auth: "admin" }), method(object({ activeOnly: boolean().optional() }), array(lifecycleJobSchema), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
16393
+ kind: "mutation",
16394
+ auth: "admin"
16395
+ }), method(object({
16327
16396
  addonId: string(),
16328
16397
  level: LogLevelSchema$1.optional()
16329
16398
  }), LogStreamEntrySchema, { kind: "subscription" });
@@ -16364,7 +16433,7 @@ Object.freeze({
16364
16433
  addonId: null,
16365
16434
  access: "create"
16366
16435
  },
16367
- "addons.cancelBulkUpdate": {
16436
+ "addons.cancelJob": {
16368
16437
  capName: "addons",
16369
16438
  capScope: "system",
16370
16439
  addonId: null,
@@ -16394,7 +16463,7 @@ Object.freeze({
16394
16463
  addonId: null,
16395
16464
  access: "view"
16396
16465
  },
16397
- "addons.getBulkUpdateState": {
16466
+ "addons.getJob": {
16398
16467
  capName: "addons",
16399
16468
  capScope: "system",
16400
16469
  addonId: null,
@@ -16442,19 +16511,19 @@ Object.freeze({
16442
16511
  addonId: null,
16443
16512
  access: "view"
16444
16513
  },
16445
- "addons.listActiveBulkUpdates": {
16514
+ "addons.listCapabilityProviders": {
16446
16515
  capName: "addons",
16447
16516
  capScope: "system",
16448
16517
  addonId: null,
16449
16518
  access: "view"
16450
16519
  },
16451
- "addons.listCapabilityProviders": {
16520
+ "addons.listFrameworkPackages": {
16452
16521
  capName: "addons",
16453
16522
  capScope: "system",
16454
16523
  addonId: null,
16455
16524
  access: "view"
16456
16525
  },
16457
- "addons.listFrameworkPackages": {
16526
+ "addons.listJobs": {
16458
16527
  capName: "addons",
16459
16528
  capScope: "system",
16460
16529
  addonId: null,
@@ -16538,7 +16607,7 @@ Object.freeze({
16538
16607
  addonId: null,
16539
16608
  access: "create"
16540
16609
  },
16541
- "addons.startBulkUpdate": {
16610
+ "addons.startJob": {
16542
16611
  capName: "addons",
16543
16612
  capScope: "system",
16544
16613
  addonId: null,
@@ -20525,6 +20594,32 @@ Object.freeze({
20525
20594
  "network-access": "ingress",
20526
20595
  "smtp-provider": "email"
20527
20596
  });
20597
+ var frameworkSwapPackageSchema = object({
20598
+ name: string(),
20599
+ stagedPath: string(),
20600
+ backupPath: string(),
20601
+ toVersion: string(),
20602
+ fromVersion: string().nullable()
20603
+ });
20604
+ object({
20605
+ jobId: string(),
20606
+ taskId: string(),
20607
+ packages: array(frameworkSwapPackageSchema),
20608
+ requestedAtMs: number(),
20609
+ schemaVersion: literal(1)
20610
+ });
20611
+ object({
20612
+ jobId: string(),
20613
+ taskId: string(),
20614
+ backups: array(object({
20615
+ name: string(),
20616
+ backupPath: string(),
20617
+ livePath: string()
20618
+ })),
20619
+ appliedAtMs: number(),
20620
+ bootAttempts: number(),
20621
+ schemaVersion: literal(1)
20622
+ });
20528
20623
  //#endregion
20529
20624
  //#region src/shared.ts
20530
20625
  /**
@@ -5155,14 +5155,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
5155
5155
  EventCategory["DeviceSleeping"] = "device.sleeping";
5156
5156
  EventCategory["RetentionCleanup"] = "retention.cleanup";
5157
5157
  /**
5158
- * Progress snapshot emitted by `BulkUpdateCoordinator` on every state
5159
- * transition (item status change, phase change, completion, cancel).
5160
- * Payload is `BulkUpdateState`. Admin UI subscribes via `useLiveEvent`
5161
- * to drive the sticky `BulkUpdateBanner` and per-row `AddonRowBadge`.
5162
- *
5163
- * Spec: docs/superpowers/specs/2026-05-21-addons-bulk-update-progress-design.md
5158
+ * Legacy bulk-update progress snapshot (payload `BulkUpdateState`). No longer
5159
+ * emitted F3 removed the coordinator that produced it; "Update all" now runs
5160
+ * as one lifecycle engine job (`AddonsJobProgress`/`AddonsJobLog`). Retained
5161
+ * (with `BulkUpdateState`) only to avoid regenerating the event maps; removed
5162
+ * in F4 once live bulk progress is re-implemented over the engine events.
5164
5163
  */
5165
5164
  EventCategory["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
5165
+ EventCategory["AddonsJobProgress"] = "addons.job-progress";
5166
+ EventCategory["AddonsJobLog"] = "addons.job-log";
5166
5167
  /**
5167
5168
  * A container's child visibility toggled (hidden/shown). Emitted by the
5168
5169
  * `accessories` cap when a child device is hidden or revealed.
@@ -16027,6 +16028,69 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
16027
16028
  kind: "mutation",
16028
16029
  auth: "admin"
16029
16030
  });
16031
+ var jobKindSchema = _enum([
16032
+ "install",
16033
+ "update",
16034
+ "uninstall",
16035
+ "restart"
16036
+ ]);
16037
+ var taskPhaseSchema = _enum([
16038
+ "queued",
16039
+ "fetching",
16040
+ "staged",
16041
+ "validating",
16042
+ "applying",
16043
+ "restarting",
16044
+ "applied",
16045
+ "done",
16046
+ "failed",
16047
+ "skipped"
16048
+ ]);
16049
+ var taskTargetSchema = _enum(["framework", "addon"]);
16050
+ var taskLogEntrySchema = object({
16051
+ tsMs: number(),
16052
+ nodeId: string(),
16053
+ packageName: string(),
16054
+ phase: taskPhaseSchema,
16055
+ message: string()
16056
+ });
16057
+ var lifecycleTaskSchema = object({
16058
+ taskId: string(),
16059
+ nodeId: string(),
16060
+ packageName: string(),
16061
+ fromVersion: string().nullable(),
16062
+ toVersion: string(),
16063
+ target: taskTargetSchema,
16064
+ phase: taskPhaseSchema,
16065
+ stagedPath: string().nullable(),
16066
+ attempts: number(),
16067
+ steps: array(taskLogEntrySchema),
16068
+ error: string().nullable(),
16069
+ startedAtMs: number().nullable(),
16070
+ finishedAtMs: number().nullable()
16071
+ });
16072
+ var lifecycleJobStateSchema = _enum([
16073
+ "running",
16074
+ "completed",
16075
+ "failed",
16076
+ "partially-failed",
16077
+ "cancelled"
16078
+ ]);
16079
+ var lifecycleJobScopeSchema = _enum([
16080
+ "single",
16081
+ "bulk",
16082
+ "cluster"
16083
+ ]);
16084
+ var lifecycleJobSchema = object({
16085
+ jobId: string(),
16086
+ kind: jobKindSchema,
16087
+ createdAtMs: number(),
16088
+ createdBy: string(),
16089
+ scope: lifecycleJobScopeSchema,
16090
+ tasks: array(lifecycleTaskSchema),
16091
+ state: lifecycleJobStateSchema,
16092
+ schemaVersion: literal(1)
16093
+ });
16030
16094
  /**
16031
16095
  * addons — system-scoped singleton capability for addon package
16032
16096
  * management (install, update, configure, restart) and per-addon log
@@ -16209,7 +16273,7 @@ var BulkUpdatePhaseSchema = _enum([
16209
16273
  "restarting",
16210
16274
  "finalizing"
16211
16275
  ]);
16212
- var BulkUpdateStateSchema = object({
16276
+ object({
16213
16277
  id: string(),
16214
16278
  nodeId: string(),
16215
16279
  startedAtMs: number(),
@@ -16312,20 +16376,7 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
16312
16376
  }), UpdateFrameworkPackageResultSchema, {
16313
16377
  kind: "mutation",
16314
16378
  auth: "admin"
16315
- }), method(object({
16316
- nodeId: string(),
16317
- items: array(object({
16318
- name: string(),
16319
- version: string(),
16320
- isSystem: boolean()
16321
- })).readonly()
16322
- }), object({ id: string() }), {
16323
- kind: "mutation",
16324
- auth: "admin"
16325
- }), method(object({ id: string() }), BulkUpdateStateSchema.nullable(), { auth: "admin" }), method(object({ id: string() }), object({ cancelled: boolean() }), {
16326
- kind: "mutation",
16327
- auth: "admin"
16328
- }), method(object({ nodeId: string().optional() }), array(BulkUpdateStateSchema).readonly(), { auth: "admin" }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
16379
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
16329
16380
  kind: "mutation",
16330
16381
  auth: "admin"
16331
16382
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -16347,6 +16398,24 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
16347
16398
  kind: "mutation",
16348
16399
  auth: "admin"
16349
16400
  }), method(CustomActionInputSchema, unknown(), { kind: "mutation" }), method(object({
16401
+ kind: _enum([
16402
+ "install",
16403
+ "update",
16404
+ "uninstall",
16405
+ "restart"
16406
+ ]),
16407
+ targets: array(object({
16408
+ name: string().min(1),
16409
+ version: string().min(1)
16410
+ })).min(1),
16411
+ nodeIds: array(string()).optional()
16412
+ }), object({ jobId: string() }), {
16413
+ kind: "mutation",
16414
+ auth: "admin"
16415
+ }), method(object({ jobId: string() }), lifecycleJobSchema.nullable(), { auth: "admin" }), method(object({ activeOnly: boolean().optional() }), array(lifecycleJobSchema), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
16416
+ kind: "mutation",
16417
+ auth: "admin"
16418
+ }), method(object({
16350
16419
  addonId: string(),
16351
16420
  level: LogLevelSchema$1.optional()
16352
16421
  }), LogStreamEntrySchema, { kind: "subscription" });
@@ -16387,7 +16456,7 @@ Object.freeze({
16387
16456
  addonId: null,
16388
16457
  access: "create"
16389
16458
  },
16390
- "addons.cancelBulkUpdate": {
16459
+ "addons.cancelJob": {
16391
16460
  capName: "addons",
16392
16461
  capScope: "system",
16393
16462
  addonId: null,
@@ -16417,7 +16486,7 @@ Object.freeze({
16417
16486
  addonId: null,
16418
16487
  access: "view"
16419
16488
  },
16420
- "addons.getBulkUpdateState": {
16489
+ "addons.getJob": {
16421
16490
  capName: "addons",
16422
16491
  capScope: "system",
16423
16492
  addonId: null,
@@ -16465,19 +16534,19 @@ Object.freeze({
16465
16534
  addonId: null,
16466
16535
  access: "view"
16467
16536
  },
16468
- "addons.listActiveBulkUpdates": {
16537
+ "addons.listCapabilityProviders": {
16469
16538
  capName: "addons",
16470
16539
  capScope: "system",
16471
16540
  addonId: null,
16472
16541
  access: "view"
16473
16542
  },
16474
- "addons.listCapabilityProviders": {
16543
+ "addons.listFrameworkPackages": {
16475
16544
  capName: "addons",
16476
16545
  capScope: "system",
16477
16546
  addonId: null,
16478
16547
  access: "view"
16479
16548
  },
16480
- "addons.listFrameworkPackages": {
16549
+ "addons.listJobs": {
16481
16550
  capName: "addons",
16482
16551
  capScope: "system",
16483
16552
  addonId: null,
@@ -16561,7 +16630,7 @@ Object.freeze({
16561
16630
  addonId: null,
16562
16631
  access: "create"
16563
16632
  },
16564
- "addons.startBulkUpdate": {
16633
+ "addons.startJob": {
16565
16634
  capName: "addons",
16566
16635
  capScope: "system",
16567
16636
  addonId: null,
@@ -20548,6 +20617,32 @@ Object.freeze({
20548
20617
  "network-access": "ingress",
20549
20618
  "smtp-provider": "email"
20550
20619
  });
20620
+ var frameworkSwapPackageSchema = object({
20621
+ name: string(),
20622
+ stagedPath: string(),
20623
+ backupPath: string(),
20624
+ toVersion: string(),
20625
+ fromVersion: string().nullable()
20626
+ });
20627
+ object({
20628
+ jobId: string(),
20629
+ taskId: string(),
20630
+ packages: array(frameworkSwapPackageSchema),
20631
+ requestedAtMs: number(),
20632
+ schemaVersion: literal(1)
20633
+ });
20634
+ object({
20635
+ jobId: string(),
20636
+ taskId: string(),
20637
+ backups: array(object({
20638
+ name: string(),
20639
+ backupPath: string(),
20640
+ livePath: string()
20641
+ })),
20642
+ appliedAtMs: number(),
20643
+ bootAttempts: number(),
20644
+ schemaVersion: literal(1)
20645
+ });
20551
20646
  //#endregion
20552
20647
  //#region src/shared.ts
20553
20648
  /**
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-B4p10WFZ.js");
5
+ const require_shared = require("./shared-uppUFqZL.js");
6
6
  let node_stream = require("node:stream");
7
7
  let webdav = require("webdav");
8
8
  //#region src/providers/webdav/webdav-config-schema.ts
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as storageProviderCapability, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as BaseAddon, t as createSessionId } from "./shared-DL9Prz1T.mjs";
1
+ import { a as safeJoinRemotePath, c as storageProviderCapability, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as BaseAddon, t as createSessionId } from "./shared-uH6t_fje.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { AuthType, createClient } from "webdav";
4
4
  //#region src/providers/webdav/webdav-config-schema.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-remote-storage",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Remote storage providers (SFTP, S3, WebDAV) — unifies remote backends behind the storage-provider cap",
5
5
  "keywords": [
6
6
  "camstack",