@camstack/addon-provider-velux 0.2.152 → 0.2.153
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/CHANGELOG.md +6 -0
- package/dist/addon.js +137 -1
- package/dist/addon.mjs +137 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.153] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- An integration deleted while its provider was down now has its orphan devices removed after two concordant reads of the integration list
|
|
15
|
+
|
|
10
16
|
## [0.2.152] - 2026-11-01
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -94566,6 +94566,88 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
94566
94566
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
94567
94567
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
94568
94568
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
94569
|
+
/**
|
|
94570
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
94571
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
94572
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
94573
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
94574
|
+
*/
|
|
94575
|
+
var OrphanIntegrationSweep = class {
|
|
94576
|
+
#logger;
|
|
94577
|
+
#minIntervalMs;
|
|
94578
|
+
#now;
|
|
94579
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
94580
|
+
constructor(options) {
|
|
94581
|
+
this.#logger = options.logger;
|
|
94582
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
94583
|
+
this.#now = options.now ?? Date.now;
|
|
94584
|
+
}
|
|
94585
|
+
get minIntervalMs() {
|
|
94586
|
+
return this.#minIntervalMs;
|
|
94587
|
+
}
|
|
94588
|
+
async sweep(input) {
|
|
94589
|
+
if (input.listedIds === null) return {
|
|
94590
|
+
newSuspects: 0,
|
|
94591
|
+
removed: []
|
|
94592
|
+
};
|
|
94593
|
+
const listed = new Set(input.listedIds);
|
|
94594
|
+
const owners = new Set(input.ownerIds);
|
|
94595
|
+
const now = this.#now();
|
|
94596
|
+
let newSuspects = 0;
|
|
94597
|
+
const removed = [];
|
|
94598
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
94599
|
+
this.#suspects.delete(id);
|
|
94600
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
94601
|
+
integrationId: id,
|
|
94602
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
94603
|
+
} });
|
|
94604
|
+
}
|
|
94605
|
+
for (const id of owners) {
|
|
94606
|
+
if (listed.has(id)) continue;
|
|
94607
|
+
const since = this.#suspects.get(id);
|
|
94608
|
+
if (since === void 0) {
|
|
94609
|
+
this.#suspects.set(id, now);
|
|
94610
|
+
newSuspects += 1;
|
|
94611
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
94612
|
+
integrationId: id,
|
|
94613
|
+
minIntervalMs: this.#minIntervalMs
|
|
94614
|
+
} });
|
|
94615
|
+
continue;
|
|
94616
|
+
}
|
|
94617
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
94618
|
+
try {
|
|
94619
|
+
const count = await input.removeDevices(id);
|
|
94620
|
+
this.#suspects.delete(id);
|
|
94621
|
+
removed.push(id);
|
|
94622
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
94623
|
+
integrationId: id,
|
|
94624
|
+
removed: count,
|
|
94625
|
+
firstMissingAt: since
|
|
94626
|
+
} });
|
|
94627
|
+
} catch (err) {
|
|
94628
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
94629
|
+
integrationId: id,
|
|
94630
|
+
error: errMsg(err)
|
|
94631
|
+
} });
|
|
94632
|
+
}
|
|
94633
|
+
}
|
|
94634
|
+
return {
|
|
94635
|
+
newSuspects,
|
|
94636
|
+
removed
|
|
94637
|
+
};
|
|
94638
|
+
}
|
|
94639
|
+
};
|
|
94640
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
94641
|
+
async function collectOwnerIntegrationIds(input) {
|
|
94642
|
+
const ids = /* @__PURE__ */ new Set();
|
|
94643
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
94644
|
+
if (d.parentDeviceId !== null) continue;
|
|
94645
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
94646
|
+
const owner = input.ownerOf(cfg);
|
|
94647
|
+
if (owner !== null) ids.add(owner);
|
|
94648
|
+
}
|
|
94649
|
+
return [...ids];
|
|
94650
|
+
}
|
|
94569
94651
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
94570
94652
|
var MB = 1024 * 1024;
|
|
94571
94653
|
1024 * MB, 3072 * MB;
|
|
@@ -94586,8 +94668,14 @@ object({
|
|
|
94586
94668
|
*/
|
|
94587
94669
|
var VeluxReconciler = class {
|
|
94588
94670
|
#deps;
|
|
94671
|
+
#sweep;
|
|
94672
|
+
#rerun = null;
|
|
94589
94673
|
constructor(deps) {
|
|
94590
94674
|
this.#deps = deps;
|
|
94675
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
94676
|
+
logger: deps.logger,
|
|
94677
|
+
...deps.sweepOptions
|
|
94678
|
+
});
|
|
94591
94679
|
}
|
|
94592
94680
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
94593
94681
|
async reconcile() {
|
|
@@ -94610,6 +94698,37 @@ var VeluxReconciler = class {
|
|
|
94610
94698
|
}
|
|
94611
94699
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
94612
94700
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
94701
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
94702
|
+
}
|
|
94703
|
+
/** Stop the scheduled second read (addon stop). */
|
|
94704
|
+
dispose() {
|
|
94705
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
94706
|
+
this.#rerun = null;
|
|
94707
|
+
}
|
|
94708
|
+
/**
|
|
94709
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
94710
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
94711
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
94712
|
+
*/
|
|
94713
|
+
async #sweepOrphans(listedIds) {
|
|
94714
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
94715
|
+
if (ownerIntegrationIds === void 0) return;
|
|
94716
|
+
try {
|
|
94717
|
+
const ownerIds = await ownerIntegrationIds();
|
|
94718
|
+
if ((await this.#sweep.sweep({
|
|
94719
|
+
ownerIds,
|
|
94720
|
+
listedIds,
|
|
94721
|
+
removeDevices
|
|
94722
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
94723
|
+
this.#rerun = setTimeout(() => {
|
|
94724
|
+
this.#rerun = null;
|
|
94725
|
+
this.reconcile();
|
|
94726
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
94727
|
+
this.#rerun.unref();
|
|
94728
|
+
}
|
|
94729
|
+
} catch (err) {
|
|
94730
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
94731
|
+
}
|
|
94613
94732
|
}
|
|
94614
94733
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
94615
94734
|
async onLifecycle(kind, integrationId) {
|
|
@@ -103662,6 +103781,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103662
103781
|
this.ctx.logger.warn("Velux: shutdown error", { meta: { error: errMsg(err) } });
|
|
103663
103782
|
}
|
|
103664
103783
|
this.clients = null;
|
|
103784
|
+
this.integrationReconciler?.dispose();
|
|
103665
103785
|
this.integrationReconciler = null;
|
|
103666
103786
|
setKlfLibraryLogSink(null);
|
|
103667
103787
|
await super.onShutdown();
|
|
@@ -103746,7 +103866,8 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103746
103866
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
103747
103867
|
return true;
|
|
103748
103868
|
},
|
|
103749
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
103869
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
103870
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
103750
103871
|
});
|
|
103751
103872
|
return this.integrationReconciler;
|
|
103752
103873
|
}
|
|
@@ -103766,6 +103887,21 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103766
103887
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
103767
103888
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
103768
103889
|
}
|
|
103890
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
103891
|
+
async ownerIntegrationIds() {
|
|
103892
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
103893
|
+
const devices = this.ctx.kernel.devices;
|
|
103894
|
+
if (!reg || !devices) return [];
|
|
103895
|
+
return await collectOwnerIntegrationIds({
|
|
103896
|
+
reg,
|
|
103897
|
+
devices,
|
|
103898
|
+
addonId: this.addonId,
|
|
103899
|
+
ownerOf: (cfg) => {
|
|
103900
|
+
const owner = cfg["integrationId"];
|
|
103901
|
+
return cfg["system"] === "velux" && typeof owner === "string" ? owner : null;
|
|
103902
|
+
}
|
|
103903
|
+
});
|
|
103904
|
+
}
|
|
103769
103905
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
103770
103906
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
103771
103907
|
const devices = this.ctx.kernel.devices;
|
package/dist/addon.mjs
CHANGED
|
@@ -94567,6 +94567,88 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
94567
94567
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
94568
94568
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
94569
94569
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
94570
|
+
/**
|
|
94571
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
94572
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
94573
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
94574
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
94575
|
+
*/
|
|
94576
|
+
var OrphanIntegrationSweep = class {
|
|
94577
|
+
#logger;
|
|
94578
|
+
#minIntervalMs;
|
|
94579
|
+
#now;
|
|
94580
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
94581
|
+
constructor(options) {
|
|
94582
|
+
this.#logger = options.logger;
|
|
94583
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
94584
|
+
this.#now = options.now ?? Date.now;
|
|
94585
|
+
}
|
|
94586
|
+
get minIntervalMs() {
|
|
94587
|
+
return this.#minIntervalMs;
|
|
94588
|
+
}
|
|
94589
|
+
async sweep(input) {
|
|
94590
|
+
if (input.listedIds === null) return {
|
|
94591
|
+
newSuspects: 0,
|
|
94592
|
+
removed: []
|
|
94593
|
+
};
|
|
94594
|
+
const listed = new Set(input.listedIds);
|
|
94595
|
+
const owners = new Set(input.ownerIds);
|
|
94596
|
+
const now = this.#now();
|
|
94597
|
+
let newSuspects = 0;
|
|
94598
|
+
const removed = [];
|
|
94599
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
94600
|
+
this.#suspects.delete(id);
|
|
94601
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
94602
|
+
integrationId: id,
|
|
94603
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
94604
|
+
} });
|
|
94605
|
+
}
|
|
94606
|
+
for (const id of owners) {
|
|
94607
|
+
if (listed.has(id)) continue;
|
|
94608
|
+
const since = this.#suspects.get(id);
|
|
94609
|
+
if (since === void 0) {
|
|
94610
|
+
this.#suspects.set(id, now);
|
|
94611
|
+
newSuspects += 1;
|
|
94612
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
94613
|
+
integrationId: id,
|
|
94614
|
+
minIntervalMs: this.#minIntervalMs
|
|
94615
|
+
} });
|
|
94616
|
+
continue;
|
|
94617
|
+
}
|
|
94618
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
94619
|
+
try {
|
|
94620
|
+
const count = await input.removeDevices(id);
|
|
94621
|
+
this.#suspects.delete(id);
|
|
94622
|
+
removed.push(id);
|
|
94623
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
94624
|
+
integrationId: id,
|
|
94625
|
+
removed: count,
|
|
94626
|
+
firstMissingAt: since
|
|
94627
|
+
} });
|
|
94628
|
+
} catch (err) {
|
|
94629
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
94630
|
+
integrationId: id,
|
|
94631
|
+
error: errMsg(err)
|
|
94632
|
+
} });
|
|
94633
|
+
}
|
|
94634
|
+
}
|
|
94635
|
+
return {
|
|
94636
|
+
newSuspects,
|
|
94637
|
+
removed
|
|
94638
|
+
};
|
|
94639
|
+
}
|
|
94640
|
+
};
|
|
94641
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
94642
|
+
async function collectOwnerIntegrationIds(input) {
|
|
94643
|
+
const ids = /* @__PURE__ */ new Set();
|
|
94644
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
94645
|
+
if (d.parentDeviceId !== null) continue;
|
|
94646
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
94647
|
+
const owner = input.ownerOf(cfg);
|
|
94648
|
+
if (owner !== null) ids.add(owner);
|
|
94649
|
+
}
|
|
94650
|
+
return [...ids];
|
|
94651
|
+
}
|
|
94570
94652
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
94571
94653
|
var MB = 1024 * 1024;
|
|
94572
94654
|
1024 * MB, 3072 * MB;
|
|
@@ -94587,8 +94669,14 @@ object({
|
|
|
94587
94669
|
*/
|
|
94588
94670
|
var VeluxReconciler = class {
|
|
94589
94671
|
#deps;
|
|
94672
|
+
#sweep;
|
|
94673
|
+
#rerun = null;
|
|
94590
94674
|
constructor(deps) {
|
|
94591
94675
|
this.#deps = deps;
|
|
94676
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
94677
|
+
logger: deps.logger,
|
|
94678
|
+
...deps.sweepOptions
|
|
94679
|
+
});
|
|
94592
94680
|
}
|
|
94593
94681
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
94594
94682
|
async reconcile() {
|
|
@@ -94611,6 +94699,37 @@ var VeluxReconciler = class {
|
|
|
94611
94699
|
}
|
|
94612
94700
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
94613
94701
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
94702
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
94703
|
+
}
|
|
94704
|
+
/** Stop the scheduled second read (addon stop). */
|
|
94705
|
+
dispose() {
|
|
94706
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
94707
|
+
this.#rerun = null;
|
|
94708
|
+
}
|
|
94709
|
+
/**
|
|
94710
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
94711
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
94712
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
94713
|
+
*/
|
|
94714
|
+
async #sweepOrphans(listedIds) {
|
|
94715
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
94716
|
+
if (ownerIntegrationIds === void 0) return;
|
|
94717
|
+
try {
|
|
94718
|
+
const ownerIds = await ownerIntegrationIds();
|
|
94719
|
+
if ((await this.#sweep.sweep({
|
|
94720
|
+
ownerIds,
|
|
94721
|
+
listedIds,
|
|
94722
|
+
removeDevices
|
|
94723
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
94724
|
+
this.#rerun = setTimeout(() => {
|
|
94725
|
+
this.#rerun = null;
|
|
94726
|
+
this.reconcile();
|
|
94727
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
94728
|
+
this.#rerun.unref();
|
|
94729
|
+
}
|
|
94730
|
+
} catch (err) {
|
|
94731
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
94732
|
+
}
|
|
94614
94733
|
}
|
|
94615
94734
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
94616
94735
|
async onLifecycle(kind, integrationId) {
|
|
@@ -103663,6 +103782,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103663
103782
|
this.ctx.logger.warn("Velux: shutdown error", { meta: { error: errMsg(err) } });
|
|
103664
103783
|
}
|
|
103665
103784
|
this.clients = null;
|
|
103785
|
+
this.integrationReconciler?.dispose();
|
|
103666
103786
|
this.integrationReconciler = null;
|
|
103667
103787
|
setKlfLibraryLogSink(null);
|
|
103668
103788
|
await super.onShutdown();
|
|
@@ -103747,7 +103867,8 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103747
103867
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
103748
103868
|
return true;
|
|
103749
103869
|
},
|
|
103750
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
103870
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
103871
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
103751
103872
|
});
|
|
103752
103873
|
return this.integrationReconciler;
|
|
103753
103874
|
}
|
|
@@ -103767,6 +103888,21 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103767
103888
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
103768
103889
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
103769
103890
|
}
|
|
103891
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
103892
|
+
async ownerIntegrationIds() {
|
|
103893
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
103894
|
+
const devices = this.ctx.kernel.devices;
|
|
103895
|
+
if (!reg || !devices) return [];
|
|
103896
|
+
return await collectOwnerIntegrationIds({
|
|
103897
|
+
reg,
|
|
103898
|
+
devices,
|
|
103899
|
+
addonId: this.addonId,
|
|
103900
|
+
ownerOf: (cfg) => {
|
|
103901
|
+
const owner = cfg["integrationId"];
|
|
103902
|
+
return cfg["system"] === "velux" && typeof owner === "string" ? owner : null;
|
|
103903
|
+
}
|
|
103904
|
+
});
|
|
103905
|
+
}
|
|
103770
103906
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
103771
103907
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
103772
103908
|
const devices = this.ctx.kernel.devices;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-velux",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.153",
|
|
4
4
|
"description": "Velux KLF-200 integration for CamStack — the gateway, local-gateway windows (with rain sensor) and shutter covers (positional, no tilt)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
64
|
"requires": {
|
|
65
|
-
"server": ">=1.2.
|
|
65
|
+
"server": ">=1.2.722"
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"files": [
|