@openmdm/core 0.8.0 → 0.9.0
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 +180 -3
- package/dist/index.js +233 -5
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +150 -2
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard.ts +40 -0
- package/src/device-identity.ts +338 -0
- package/src/index.ts +165 -5
- package/src/types.ts +155 -1
package/src/types.ts
CHANGED
|
@@ -32,6 +32,23 @@ export interface Device {
|
|
|
32
32
|
lastHeartbeat?: Date | null;
|
|
33
33
|
lastSync?: Date | null;
|
|
34
34
|
|
|
35
|
+
// Device identity (Phase 2b — device-pinned ECDSA P-256 key)
|
|
36
|
+
/**
|
|
37
|
+
* Base64-encoded SPKI public key the device registered on first
|
|
38
|
+
* enrollment. Requests from this device can be verified against
|
|
39
|
+
* this key via `verifyDeviceRequest` / `verifyEcdsaSignature` from
|
|
40
|
+
* `@openmdm/core`. `null` on devices that enrolled via the legacy
|
|
41
|
+
* HMAC path and have never been migrated.
|
|
42
|
+
*/
|
|
43
|
+
publicKey?: string | null;
|
|
44
|
+
/**
|
|
45
|
+
* How the device originally enrolled. `'hmac'` for the legacy
|
|
46
|
+
* shared-secret path; `'pinned-key'` for the device-pinned ECDSA
|
|
47
|
+
* path. `null` on pre-Phase-2b device rows that predate the
|
|
48
|
+
* column (treated as `'hmac'`).
|
|
49
|
+
*/
|
|
50
|
+
enrollmentMethod?: 'hmac' | 'pinned-key' | null;
|
|
51
|
+
|
|
35
52
|
// Telemetry
|
|
36
53
|
batteryLevel?: number | null;
|
|
37
54
|
storageUsed?: number | null;
|
|
@@ -94,6 +111,10 @@ export interface UpdateDeviceInput {
|
|
|
94
111
|
location?: DeviceLocation;
|
|
95
112
|
tags?: Record<string, string>;
|
|
96
113
|
metadata?: Record<string, unknown>;
|
|
114
|
+
/** Phase 2b — pin a new public key on first enroll. */
|
|
115
|
+
publicKey?: string | null;
|
|
116
|
+
/** Phase 2b — record which auth path the device enrolled via. */
|
|
117
|
+
enrollmentMethod?: 'hmac' | 'pinned-key' | null;
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
export interface DeviceFilter {
|
|
@@ -532,8 +553,45 @@ export interface EnrollmentRequest {
|
|
|
532
553
|
// Enrollment details
|
|
533
554
|
method: EnrollmentMethod;
|
|
534
555
|
timestamp: string;
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Signature over the canonical enrollment message.
|
|
559
|
+
*
|
|
560
|
+
* Phase 2a (HMAC path, backwards-compatible): hex-encoded
|
|
561
|
+
* HMAC-SHA256 of the nine-field pipe-delimited canonical form
|
|
562
|
+
* (see `concepts/enrollment`).
|
|
563
|
+
*
|
|
564
|
+
* Phase 2b (device-pinned-key path, preferred): base64-encoded
|
|
565
|
+
* DER ECDSA-P256 signature produced by the device's Keystore
|
|
566
|
+
* private key, over `canonicalEnrollmentMessage(...)` including
|
|
567
|
+
* the public key and challenge. The server distinguishes the
|
|
568
|
+
* two paths by whether `publicKey` is present on the request.
|
|
569
|
+
*/
|
|
535
570
|
signature: string;
|
|
536
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Base64-encoded SPKI public key (EC P-256) the device generated
|
|
574
|
+
* in its Keystore. When present, enrollment follows the Phase 2b
|
|
575
|
+
* device-pinned-key path and `signature` must verify as an ECDSA
|
|
576
|
+
* signature against this key. The server pins this key on the
|
|
577
|
+
* device row on first successful enroll; any future enroll
|
|
578
|
+
* attempting a different key for the same `enrollmentId` is
|
|
579
|
+
* rejected with `PublicKeyMismatchError`.
|
|
580
|
+
*
|
|
581
|
+
* Omit for the legacy HMAC path. Callers that want to migrate a
|
|
582
|
+
* fleet gradually can run both paths in parallel.
|
|
583
|
+
*/
|
|
584
|
+
publicKey?: string;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Opaque challenge issued by `GET /agent/enroll/challenge`. Must
|
|
588
|
+
* be present whenever `publicKey` is present — the server uses
|
|
589
|
+
* it to prevent replay of captured enrollment payloads. The
|
|
590
|
+
* challenge is single-use: the server consumes it on first
|
|
591
|
+
* successful verify.
|
|
592
|
+
*/
|
|
593
|
+
attestationChallenge?: string;
|
|
594
|
+
|
|
537
595
|
// Optional pre-assigned policy/group
|
|
538
596
|
policyId?: string;
|
|
539
597
|
groupId?: string;
|
|
@@ -551,6 +609,43 @@ export interface EnrollmentResponse {
|
|
|
551
609
|
tokenExpiresAt?: Date;
|
|
552
610
|
}
|
|
553
611
|
|
|
612
|
+
// ============================================
|
|
613
|
+
// Device Identity (Phase 2b)
|
|
614
|
+
// ============================================
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Single-use nonce issued by `GET /agent/enroll/challenge` and
|
|
618
|
+
* consumed on first successful verify of a device-pinned-key
|
|
619
|
+
* enrollment. A persisted record; the `consume*` adapter methods
|
|
620
|
+
* enforce the single-use invariant.
|
|
621
|
+
*/
|
|
622
|
+
export interface EnrollmentChallenge {
|
|
623
|
+
challenge: string;
|
|
624
|
+
expiresAt: Date;
|
|
625
|
+
consumedAt?: Date | null;
|
|
626
|
+
createdAt: Date;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Result of calling `verifyDeviceRequest`. Callers pattern-match on
|
|
631
|
+
* `ok` and, when `false`, on `reason` to decide their response
|
|
632
|
+
* shape:
|
|
633
|
+
*
|
|
634
|
+
* - `not-found` — unknown device id. Return 401.
|
|
635
|
+
* - `no-pinned-key` — device exists but never migrated off the
|
|
636
|
+
* legacy HMAC path. Caller should fall back
|
|
637
|
+
* to their HMAC verifier, or fail if
|
|
638
|
+
* they've completed their migration.
|
|
639
|
+
* - `signature-invalid` — signature did not verify against the
|
|
640
|
+
* pinned key. Return 401. Never re-pin
|
|
641
|
+
* in response to this.
|
|
642
|
+
*/
|
|
643
|
+
export type DeviceIdentityVerification =
|
|
644
|
+
| { ok: true; device: Device }
|
|
645
|
+
| { ok: false; reason: 'not-found' }
|
|
646
|
+
| { ok: false; reason: 'no-pinned-key'; device: Device }
|
|
647
|
+
| { ok: false; reason: 'signature-invalid'; device: Device };
|
|
648
|
+
|
|
554
649
|
export interface PushConfig {
|
|
555
650
|
provider: 'fcm' | 'mqtt' | 'websocket' | 'polling';
|
|
556
651
|
fcmSenderId?: string;
|
|
@@ -844,7 +939,7 @@ export interface PushProviderConfig {
|
|
|
844
939
|
export interface EnrollmentConfig {
|
|
845
940
|
/** Auto-enroll devices with valid signature */
|
|
846
941
|
autoEnroll?: boolean;
|
|
847
|
-
/** HMAC secret for device signature verification */
|
|
942
|
+
/** HMAC secret for device signature verification (Phase 2a path) */
|
|
848
943
|
deviceSecret: string;
|
|
849
944
|
/** Allowed enrollment methods */
|
|
850
945
|
allowedMethods?: EnrollmentMethod[];
|
|
@@ -856,6 +951,35 @@ export interface EnrollmentConfig {
|
|
|
856
951
|
requireApproval?: boolean;
|
|
857
952
|
/** Custom enrollment validation */
|
|
858
953
|
validate?: (request: EnrollmentRequest) => Promise<boolean>;
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Phase 2b device-pinned-key configuration. Optional — when
|
|
957
|
+
* omitted, enrollment continues to accept the Phase 2a HMAC path
|
|
958
|
+
* exclusively, matching pre-0.9 behaviour.
|
|
959
|
+
*/
|
|
960
|
+
pinnedKey?: PinnedKeyConfig;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Device-pinned-key enrollment options. See
|
|
965
|
+
* `docs/concepts/enrollment` for the full flow.
|
|
966
|
+
*/
|
|
967
|
+
export interface PinnedKeyConfig {
|
|
968
|
+
/**
|
|
969
|
+
* Require every new enrollment to use the pinned-key path. When
|
|
970
|
+
* `true`, requests without `publicKey` are rejected. When `false`
|
|
971
|
+
* (the default), both paths coexist during rollout — the server
|
|
972
|
+
* pins a public key when one is provided, falls back to HMAC when
|
|
973
|
+
* it isn't.
|
|
974
|
+
*/
|
|
975
|
+
required?: boolean;
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* TTL for enrollment challenges, in seconds. Defaults to 300
|
|
979
|
+
* (5 minutes). Challenges are single-use; this only bounds how
|
|
980
|
+
* long an unused challenge stays valid.
|
|
981
|
+
*/
|
|
982
|
+
challengeTtlSeconds?: number;
|
|
859
983
|
}
|
|
860
984
|
|
|
861
985
|
// ============================================
|
|
@@ -935,6 +1059,36 @@ export interface DatabaseAdapter {
|
|
|
935
1059
|
moveGroup?(groupId: string, newParentId: string | null): Promise<Group>;
|
|
936
1060
|
getGroupHierarchyStats?(): Promise<GroupHierarchyStats>;
|
|
937
1061
|
|
|
1062
|
+
// Enrollment challenges (optional - for Phase 2b device-pinned-key)
|
|
1063
|
+
/**
|
|
1064
|
+
* Persist a new single-use enrollment challenge. The adapter
|
|
1065
|
+
* should store it with `consumed_at = null` and enforce a
|
|
1066
|
+
* primary-key constraint on `challenge` so duplicate inserts
|
|
1067
|
+
* fail loudly.
|
|
1068
|
+
*/
|
|
1069
|
+
createEnrollmentChallenge?(challenge: EnrollmentChallenge): Promise<void>;
|
|
1070
|
+
/**
|
|
1071
|
+
* Look up a challenge by its opaque value. Returns `null` if not
|
|
1072
|
+
* found. Does NOT filter on expiry — the core layer checks
|
|
1073
|
+
* freshness so the adapter stays dumb.
|
|
1074
|
+
*/
|
|
1075
|
+
findEnrollmentChallenge?(challenge: string): Promise<EnrollmentChallenge | null>;
|
|
1076
|
+
/**
|
|
1077
|
+
* Atomically mark a challenge as consumed. Must set
|
|
1078
|
+
* `consumed_at = now()` and return the updated row only when the
|
|
1079
|
+
* challenge was previously unused. Adapters should implement
|
|
1080
|
+
* this as a conditional UPDATE (e.g. Postgres
|
|
1081
|
+
* `UPDATE ... WHERE consumed_at IS NULL RETURNING *`) so two
|
|
1082
|
+
* concurrent consume attempts cannot both succeed.
|
|
1083
|
+
*/
|
|
1084
|
+
consumeEnrollmentChallenge?(challenge: string): Promise<EnrollmentChallenge | null>;
|
|
1085
|
+
/**
|
|
1086
|
+
* Delete expired, unconsumed challenges. Called periodically by
|
|
1087
|
+
* the core layer; adapters can no-op if they rely on a TTL index
|
|
1088
|
+
* elsewhere.
|
|
1089
|
+
*/
|
|
1090
|
+
pruneExpiredEnrollmentChallenges?(now: Date): Promise<number>;
|
|
1091
|
+
|
|
938
1092
|
// Tenants (optional - for multi-tenancy)
|
|
939
1093
|
findTenant?(id: string): Promise<Tenant | null>;
|
|
940
1094
|
findTenantBySlug?(slug: string): Promise<Tenant | null>;
|