@camstack/addon-provider-rademacher 0.2.155 → 0.2.156
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.156] - 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.155] - 2026-11-01
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -94487,6 +94487,88 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
94487
94487
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
94488
94488
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
94489
94489
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
94490
|
+
/**
|
|
94491
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
94492
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
94493
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
94494
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
94495
|
+
*/
|
|
94496
|
+
var OrphanIntegrationSweep = class {
|
|
94497
|
+
#logger;
|
|
94498
|
+
#minIntervalMs;
|
|
94499
|
+
#now;
|
|
94500
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
94501
|
+
constructor(options) {
|
|
94502
|
+
this.#logger = options.logger;
|
|
94503
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
94504
|
+
this.#now = options.now ?? Date.now;
|
|
94505
|
+
}
|
|
94506
|
+
get minIntervalMs() {
|
|
94507
|
+
return this.#minIntervalMs;
|
|
94508
|
+
}
|
|
94509
|
+
async sweep(input) {
|
|
94510
|
+
if (input.listedIds === null) return {
|
|
94511
|
+
newSuspects: 0,
|
|
94512
|
+
removed: []
|
|
94513
|
+
};
|
|
94514
|
+
const listed = new Set(input.listedIds);
|
|
94515
|
+
const owners = new Set(input.ownerIds);
|
|
94516
|
+
const now = this.#now();
|
|
94517
|
+
let newSuspects = 0;
|
|
94518
|
+
const removed = [];
|
|
94519
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
94520
|
+
this.#suspects.delete(id);
|
|
94521
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
94522
|
+
integrationId: id,
|
|
94523
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
94524
|
+
} });
|
|
94525
|
+
}
|
|
94526
|
+
for (const id of owners) {
|
|
94527
|
+
if (listed.has(id)) continue;
|
|
94528
|
+
const since = this.#suspects.get(id);
|
|
94529
|
+
if (since === void 0) {
|
|
94530
|
+
this.#suspects.set(id, now);
|
|
94531
|
+
newSuspects += 1;
|
|
94532
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
94533
|
+
integrationId: id,
|
|
94534
|
+
minIntervalMs: this.#minIntervalMs
|
|
94535
|
+
} });
|
|
94536
|
+
continue;
|
|
94537
|
+
}
|
|
94538
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
94539
|
+
try {
|
|
94540
|
+
const count = await input.removeDevices(id);
|
|
94541
|
+
this.#suspects.delete(id);
|
|
94542
|
+
removed.push(id);
|
|
94543
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
94544
|
+
integrationId: id,
|
|
94545
|
+
removed: count,
|
|
94546
|
+
firstMissingAt: since
|
|
94547
|
+
} });
|
|
94548
|
+
} catch (err) {
|
|
94549
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
94550
|
+
integrationId: id,
|
|
94551
|
+
error: errMsg(err)
|
|
94552
|
+
} });
|
|
94553
|
+
}
|
|
94554
|
+
}
|
|
94555
|
+
return {
|
|
94556
|
+
newSuspects,
|
|
94557
|
+
removed
|
|
94558
|
+
};
|
|
94559
|
+
}
|
|
94560
|
+
};
|
|
94561
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
94562
|
+
async function collectOwnerIntegrationIds(input) {
|
|
94563
|
+
const ids = /* @__PURE__ */ new Set();
|
|
94564
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
94565
|
+
if (d.parentDeviceId !== null) continue;
|
|
94566
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
94567
|
+
const owner = input.ownerOf(cfg);
|
|
94568
|
+
if (owner !== null) ids.add(owner);
|
|
94569
|
+
}
|
|
94570
|
+
return [...ids];
|
|
94571
|
+
}
|
|
94490
94572
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
94491
94573
|
var MB = 1024 * 1024;
|
|
94492
94574
|
1024 * MB, 3072 * MB;
|
|
@@ -94507,8 +94589,14 @@ object({
|
|
|
94507
94589
|
*/
|
|
94508
94590
|
var RademacherReconciler = class {
|
|
94509
94591
|
#deps;
|
|
94592
|
+
#sweep;
|
|
94593
|
+
#rerun = null;
|
|
94510
94594
|
constructor(deps) {
|
|
94511
94595
|
this.#deps = deps;
|
|
94596
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
94597
|
+
logger: deps.logger,
|
|
94598
|
+
...deps.sweepOptions
|
|
94599
|
+
});
|
|
94512
94600
|
}
|
|
94513
94601
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
94514
94602
|
async reconcile() {
|
|
@@ -94531,6 +94619,37 @@ var RademacherReconciler = class {
|
|
|
94531
94619
|
}
|
|
94532
94620
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
94533
94621
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
94622
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
94623
|
+
}
|
|
94624
|
+
/** Stop the scheduled second read (addon stop). */
|
|
94625
|
+
dispose() {
|
|
94626
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
94627
|
+
this.#rerun = null;
|
|
94628
|
+
}
|
|
94629
|
+
/**
|
|
94630
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
94631
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
94632
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
94633
|
+
*/
|
|
94634
|
+
async #sweepOrphans(listedIds) {
|
|
94635
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
94636
|
+
if (ownerIntegrationIds === void 0) return;
|
|
94637
|
+
try {
|
|
94638
|
+
const ownerIds = await ownerIntegrationIds();
|
|
94639
|
+
if ((await this.#sweep.sweep({
|
|
94640
|
+
ownerIds,
|
|
94641
|
+
listedIds,
|
|
94642
|
+
removeDevices
|
|
94643
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
94644
|
+
this.#rerun = setTimeout(() => {
|
|
94645
|
+
this.#rerun = null;
|
|
94646
|
+
this.reconcile();
|
|
94647
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
94648
|
+
this.#rerun.unref();
|
|
94649
|
+
}
|
|
94650
|
+
} catch (err) {
|
|
94651
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
94652
|
+
}
|
|
94534
94653
|
}
|
|
94535
94654
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
94536
94655
|
async onLifecycle(kind, integrationId) {
|
|
@@ -96739,6 +96858,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
96739
96858
|
this.ctx.logger.warn("Rademacher: shutdown error", { meta: { error: errMsg(err) } });
|
|
96740
96859
|
}
|
|
96741
96860
|
this.clients = null;
|
|
96861
|
+
this.integrationReconciler?.dispose();
|
|
96742
96862
|
this.integrationReconciler = null;
|
|
96743
96863
|
await super.onShutdown();
|
|
96744
96864
|
}
|
|
@@ -96781,7 +96901,8 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
96781
96901
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
96782
96902
|
return true;
|
|
96783
96903
|
},
|
|
96784
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
96904
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
96905
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
96785
96906
|
});
|
|
96786
96907
|
return this.integrationReconciler;
|
|
96787
96908
|
}
|
|
@@ -96801,6 +96922,21 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
96801
96922
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
96802
96923
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
96803
96924
|
}
|
|
96925
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
96926
|
+
async ownerIntegrationIds() {
|
|
96927
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
96928
|
+
const devices = this.ctx.kernel.devices;
|
|
96929
|
+
if (!reg || !devices) return [];
|
|
96930
|
+
return await collectOwnerIntegrationIds({
|
|
96931
|
+
reg,
|
|
96932
|
+
devices,
|
|
96933
|
+
addonId: this.addonId,
|
|
96934
|
+
ownerOf: (cfg) => {
|
|
96935
|
+
const owner = cfg["integrationId"];
|
|
96936
|
+
return cfg["system"] === "rademacher" && typeof owner === "string" ? owner : null;
|
|
96937
|
+
}
|
|
96938
|
+
});
|
|
96939
|
+
}
|
|
96804
96940
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
96805
96941
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
96806
96942
|
const devices = this.ctx.kernel.devices;
|
package/dist/addon.mjs
CHANGED
|
@@ -94486,6 +94486,88 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
94486
94486
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
94487
94487
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
94488
94488
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
94489
|
+
/**
|
|
94490
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
94491
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
94492
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
94493
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
94494
|
+
*/
|
|
94495
|
+
var OrphanIntegrationSweep = class {
|
|
94496
|
+
#logger;
|
|
94497
|
+
#minIntervalMs;
|
|
94498
|
+
#now;
|
|
94499
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
94500
|
+
constructor(options) {
|
|
94501
|
+
this.#logger = options.logger;
|
|
94502
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
94503
|
+
this.#now = options.now ?? Date.now;
|
|
94504
|
+
}
|
|
94505
|
+
get minIntervalMs() {
|
|
94506
|
+
return this.#minIntervalMs;
|
|
94507
|
+
}
|
|
94508
|
+
async sweep(input) {
|
|
94509
|
+
if (input.listedIds === null) return {
|
|
94510
|
+
newSuspects: 0,
|
|
94511
|
+
removed: []
|
|
94512
|
+
};
|
|
94513
|
+
const listed = new Set(input.listedIds);
|
|
94514
|
+
const owners = new Set(input.ownerIds);
|
|
94515
|
+
const now = this.#now();
|
|
94516
|
+
let newSuspects = 0;
|
|
94517
|
+
const removed = [];
|
|
94518
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
94519
|
+
this.#suspects.delete(id);
|
|
94520
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
94521
|
+
integrationId: id,
|
|
94522
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
94523
|
+
} });
|
|
94524
|
+
}
|
|
94525
|
+
for (const id of owners) {
|
|
94526
|
+
if (listed.has(id)) continue;
|
|
94527
|
+
const since = this.#suspects.get(id);
|
|
94528
|
+
if (since === void 0) {
|
|
94529
|
+
this.#suspects.set(id, now);
|
|
94530
|
+
newSuspects += 1;
|
|
94531
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
94532
|
+
integrationId: id,
|
|
94533
|
+
minIntervalMs: this.#minIntervalMs
|
|
94534
|
+
} });
|
|
94535
|
+
continue;
|
|
94536
|
+
}
|
|
94537
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
94538
|
+
try {
|
|
94539
|
+
const count = await input.removeDevices(id);
|
|
94540
|
+
this.#suspects.delete(id);
|
|
94541
|
+
removed.push(id);
|
|
94542
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
94543
|
+
integrationId: id,
|
|
94544
|
+
removed: count,
|
|
94545
|
+
firstMissingAt: since
|
|
94546
|
+
} });
|
|
94547
|
+
} catch (err) {
|
|
94548
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
94549
|
+
integrationId: id,
|
|
94550
|
+
error: errMsg(err)
|
|
94551
|
+
} });
|
|
94552
|
+
}
|
|
94553
|
+
}
|
|
94554
|
+
return {
|
|
94555
|
+
newSuspects,
|
|
94556
|
+
removed
|
|
94557
|
+
};
|
|
94558
|
+
}
|
|
94559
|
+
};
|
|
94560
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
94561
|
+
async function collectOwnerIntegrationIds(input) {
|
|
94562
|
+
const ids = /* @__PURE__ */ new Set();
|
|
94563
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
94564
|
+
if (d.parentDeviceId !== null) continue;
|
|
94565
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
94566
|
+
const owner = input.ownerOf(cfg);
|
|
94567
|
+
if (owner !== null) ids.add(owner);
|
|
94568
|
+
}
|
|
94569
|
+
return [...ids];
|
|
94570
|
+
}
|
|
94489
94571
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
94490
94572
|
var MB = 1024 * 1024;
|
|
94491
94573
|
1024 * MB, 3072 * MB;
|
|
@@ -94506,8 +94588,14 @@ object({
|
|
|
94506
94588
|
*/
|
|
94507
94589
|
var RademacherReconciler = class {
|
|
94508
94590
|
#deps;
|
|
94591
|
+
#sweep;
|
|
94592
|
+
#rerun = null;
|
|
94509
94593
|
constructor(deps) {
|
|
94510
94594
|
this.#deps = deps;
|
|
94595
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
94596
|
+
logger: deps.logger,
|
|
94597
|
+
...deps.sweepOptions
|
|
94598
|
+
});
|
|
94511
94599
|
}
|
|
94512
94600
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
94513
94601
|
async reconcile() {
|
|
@@ -94530,6 +94618,37 @@ var RademacherReconciler = class {
|
|
|
94530
94618
|
}
|
|
94531
94619
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
94532
94620
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
94621
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
94622
|
+
}
|
|
94623
|
+
/** Stop the scheduled second read (addon stop). */
|
|
94624
|
+
dispose() {
|
|
94625
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
94626
|
+
this.#rerun = null;
|
|
94627
|
+
}
|
|
94628
|
+
/**
|
|
94629
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
94630
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
94631
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
94632
|
+
*/
|
|
94633
|
+
async #sweepOrphans(listedIds) {
|
|
94634
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
94635
|
+
if (ownerIntegrationIds === void 0) return;
|
|
94636
|
+
try {
|
|
94637
|
+
const ownerIds = await ownerIntegrationIds();
|
|
94638
|
+
if ((await this.#sweep.sweep({
|
|
94639
|
+
ownerIds,
|
|
94640
|
+
listedIds,
|
|
94641
|
+
removeDevices
|
|
94642
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
94643
|
+
this.#rerun = setTimeout(() => {
|
|
94644
|
+
this.#rerun = null;
|
|
94645
|
+
this.reconcile();
|
|
94646
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
94647
|
+
this.#rerun.unref();
|
|
94648
|
+
}
|
|
94649
|
+
} catch (err) {
|
|
94650
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
94651
|
+
}
|
|
94533
94652
|
}
|
|
94534
94653
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
94535
94654
|
async onLifecycle(kind, integrationId) {
|
|
@@ -96738,6 +96857,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
96738
96857
|
this.ctx.logger.warn("Rademacher: shutdown error", { meta: { error: errMsg(err) } });
|
|
96739
96858
|
}
|
|
96740
96859
|
this.clients = null;
|
|
96860
|
+
this.integrationReconciler?.dispose();
|
|
96741
96861
|
this.integrationReconciler = null;
|
|
96742
96862
|
await super.onShutdown();
|
|
96743
96863
|
}
|
|
@@ -96780,7 +96900,8 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
96780
96900
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
96781
96901
|
return true;
|
|
96782
96902
|
},
|
|
96783
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
96903
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
96904
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
96784
96905
|
});
|
|
96785
96906
|
return this.integrationReconciler;
|
|
96786
96907
|
}
|
|
@@ -96800,6 +96921,21 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
96800
96921
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
96801
96922
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
96802
96923
|
}
|
|
96924
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
96925
|
+
async ownerIntegrationIds() {
|
|
96926
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
96927
|
+
const devices = this.ctx.kernel.devices;
|
|
96928
|
+
if (!reg || !devices) return [];
|
|
96929
|
+
return await collectOwnerIntegrationIds({
|
|
96930
|
+
reg,
|
|
96931
|
+
devices,
|
|
96932
|
+
addonId: this.addonId,
|
|
96933
|
+
ownerOf: (cfg) => {
|
|
96934
|
+
const owner = cfg["integrationId"];
|
|
96935
|
+
return cfg["system"] === "rademacher" && typeof owner === "string" ? owner : null;
|
|
96936
|
+
}
|
|
96937
|
+
});
|
|
96938
|
+
}
|
|
96803
96939
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
96804
96940
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
96805
96941
|
const devices = this.ctx.kernel.devices;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-rademacher",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.156",
|
|
4
4
|
"description": "Rademacher HomePilot device-provider addon for CamStack — wraps the @apocaliss92/noderademacher local-hub client (roller shutters over the cover cap)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
}
|
|
64
64
|
],
|
|
65
65
|
"requires": {
|
|
66
|
-
"server": ">=1.2.
|
|
66
|
+
"server": ">=1.2.722"
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"files": [
|