@hello.nrfcloud.com/nrfcloud-api-helpers 1.0.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.
Files changed (46) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +15 -0
  3. package/dist/api/DeviceShadow.d.ts +17 -0
  4. package/dist/api/DeviceShadow.js +18 -0
  5. package/dist/api/DeviceShadow.spec.d.ts +1 -0
  6. package/dist/api/DeviceShadow.spec.js +53 -0
  7. package/dist/api/createAccountDevice.d.ts +13 -0
  8. package/dist/api/createAccountDevice.js +20 -0
  9. package/dist/api/deleteAccountDevice.d.ts +4 -0
  10. package/dist/api/deleteAccountDevice.js +9 -0
  11. package/dist/api/devices.d.ts +152 -0
  12. package/dist/api/devices.js +121 -0
  13. package/dist/api/getAccountInfo.d.ts +18 -0
  14. package/dist/api/getAccountInfo.js +21 -0
  15. package/dist/api/getAccountInfo.spec.d.ts +1 -0
  16. package/dist/api/getAccountInfo.spec.js +25 -0
  17. package/dist/api/getCurrentMonthlyCosts.d.ts +9 -0
  18. package/dist/api/getCurrentMonthlyCosts.js +15 -0
  19. package/dist/api/getCurrentMonthlyCosts.spec.d.ts +1 -0
  20. package/dist/api/getCurrentMonthlyCosts.spec.js +17 -0
  21. package/dist/api/getDeviceShadow.d.ts +23 -0
  22. package/dist/api/getDeviceShadow.js +33 -0
  23. package/dist/api/getDeviceShadow.spec.d.ts +1 -0
  24. package/dist/api/getDeviceShadow.spec.js +28 -0
  25. package/dist/api/index.d.ts +9 -0
  26. package/dist/api/index.js +9 -0
  27. package/dist/api/slashless.d.ts +1 -0
  28. package/dist/api/slashless.js +1 -0
  29. package/dist/api/slashless.spec.d.ts +1 -0
  30. package/dist/api/slashless.spec.js +6 -0
  31. package/dist/api/test-data/account.json +63 -0
  32. package/dist/api/validatedFetch.d.ts +30 -0
  33. package/dist/api/validatedFetch.js +54 -0
  34. package/dist/api/validatedFetch.spec.d.ts +1 -0
  35. package/dist/api/validatedFetch.spec.js +101 -0
  36. package/dist/settings/getAllAccounts.d.ts +5 -0
  37. package/dist/settings/getAllAccounts.js +9 -0
  38. package/dist/settings/index.d.ts +4 -0
  39. package/dist/settings/index.js +4 -0
  40. package/dist/settings/initializeAccount.d.ts +10 -0
  41. package/dist/settings/initializeAccount.js +72 -0
  42. package/dist/settings/scope.d.ts +17 -0
  43. package/dist/settings/scope.js +34 -0
  44. package/dist/settings/settings.d.ts +43 -0
  45. package/dist/settings/settings.js +85 -0
  46. package/package.json +95 -0
@@ -0,0 +1,33 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { validatedFetch } from './validatedFetch.js';
3
+ import { DeviceShadow } from './DeviceShadow.js';
4
+ const DeviceShadows = Type.Array(DeviceShadow);
5
+ /**
6
+ * @see https://api.nrfcloud.com/v1#tag/All-Devices/operation/ListDevices
7
+ */
8
+ const ListDevices = Type.Object({
9
+ items: DeviceShadows,
10
+ total: Type.Optional(Type.Number({ minimum: 0 })),
11
+ pageNextToken: Type.Optional(Type.String({ minLength: 1 })),
12
+ });
13
+ export const getDeviceShadow = ({ endpoint, apiKey, }, fetchImplementation) => {
14
+ const vf = validatedFetch({ endpoint, apiKey }, fetchImplementation);
15
+ return async (devices) => {
16
+ const params = {
17
+ includeState: true,
18
+ includeStateMeta: true,
19
+ pageLimit: 100,
20
+ deviceIds: devices.join(','),
21
+ };
22
+ const queryString = Object.entries(params)
23
+ .sort((a, b) => a[0].localeCompare(b[0]))
24
+ .map((kv) => kv.map(encodeURIComponent).join('='))
25
+ .join('&');
26
+ const url = `devices?${queryString}`;
27
+ const maybeResult = await vf({ resource: url }, ListDevices);
28
+ if ('error' in maybeResult) {
29
+ return { error: maybeResult.error };
30
+ }
31
+ return { shadows: maybeResult.result.items };
32
+ };
33
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { getDeviceShadow } from './getDeviceShadow.js';
4
+ void describe('getDeviceShadow()', () => {
5
+ void it('should accept a response without pagination and total devices', async () => {
6
+ const mockFetch = mock.fn(() => ({
7
+ ok: true,
8
+ json: async () => Promise.resolve({
9
+ items: [],
10
+ }),
11
+ }));
12
+ const fetcher = getDeviceShadow({
13
+ endpoint: new URL('https://example.com/'),
14
+ apiKey: 'some-key',
15
+ }, mockFetch);
16
+ const res = await fetcher(['device-id']);
17
+ assert.deepEqual(mockFetch.mock.calls[0]?.arguments, [
18
+ `https://example.com/v1/devices?deviceIds=device-id&includeState=true&includeStateMeta=true&pageLimit=100`,
19
+ {
20
+ headers: {
21
+ Accept: 'application/json; charset=utf-8',
22
+ Authorization: 'Bearer some-key',
23
+ },
24
+ },
25
+ ]);
26
+ assert.deepEqual(res, { shadows: [] });
27
+ });
28
+ });
@@ -0,0 +1,9 @@
1
+ export * from './validatedFetch.js';
2
+ export * from './devices.js';
3
+ export * from './getCurrentMonthlyCosts.js';
4
+ export * from './createAccountDevice.js';
5
+ export * from './slashless.js';
6
+ export * from './getAccountInfo.js';
7
+ export * from './deleteAccountDevice.js';
8
+ export * from './getDeviceShadow.js';
9
+ export * from './DeviceShadow.js';
@@ -0,0 +1,9 @@
1
+ export * from './validatedFetch.js';
2
+ export * from './devices.js';
3
+ export * from './getCurrentMonthlyCosts.js';
4
+ export * from './createAccountDevice.js';
5
+ export * from './slashless.js';
6
+ export * from './getAccountInfo.js';
7
+ export * from './deleteAccountDevice.js';
8
+ export * from './getDeviceShadow.js';
9
+ export * from './DeviceShadow.js';
@@ -0,0 +1 @@
1
+ export declare const slashless: (url: URL) => string;
@@ -0,0 +1 @@
1
+ export const slashless = (url) => url.toString().replace(/\/$/, '');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { slashless } from './slashless.js';
4
+ void describe('slashless()', () => {
5
+ void it('should remove the slash from an URL and convert it to a string', () => assert.equal(slashless(new URL('https://api.nrfcloud.com/')), 'https://api.nrfcloud.com'));
6
+ });
@@ -0,0 +1,63 @@
1
+ {
2
+ "mqttEndpoint": "mqtt.nrfcloud.com",
3
+ "mqttTopicPrefix": "prod/b8b26bc5-2814-4063-b4fa-83ecddb2fec7/",
4
+ "team": {
5
+ "tenantId": "b8b26bc5-2814-4063-b4fa-83ecddb2fec7",
6
+ "name": "XXX"
7
+ },
8
+ "role": "owner",
9
+ "tags": [],
10
+ "plan": {
11
+ "name": "ENTERPRISE",
12
+ "proxyUsageDeclarations": {
13
+ "AGPS": 0,
14
+ "PGPS": 0,
15
+ "GROUND_FIX": 0
16
+ },
17
+ "serviceKeys": [
18
+ {
19
+ "service": "ALL",
20
+ "enabled": true,
21
+ "createdAt": "2023-09-14T10:24:31.663Z"
22
+ }
23
+ ],
24
+ "currentMonthCosts": [
25
+ {
26
+ "serviceId": "Devices",
27
+ "serviceDescription": "Devices in your account",
28
+ "quantity": 53,
29
+ "price": 0.1,
30
+ "total": 5.3
31
+ },
32
+ {
33
+ "serviceId": "Messages",
34
+ "serviceDescription": "Device messages stored",
35
+ "quantity": 70281,
36
+ "price": 0.0001,
37
+ "total": 7.03
38
+ },
39
+ {
40
+ "serviceId": "AGPS",
41
+ "serviceDescription": "Assisted GPS Service: total requests",
42
+ "quantity": 8,
43
+ "price": 0.001,
44
+ "total": 0.01
45
+ },
46
+ {
47
+ "serviceId": "SCELL",
48
+ "serviceDescription": "Single-Cell Location Service: total requests",
49
+ "quantity": 2684,
50
+ "price": 0.001,
51
+ "total": 2.68
52
+ },
53
+ {
54
+ "serviceId": "MCELL",
55
+ "serviceDescription": "Multi-Cell Location Service: total requests",
56
+ "quantity": 4505,
57
+ "price": 0.002,
58
+ "total": 9.01
59
+ }
60
+ ],
61
+ "currentMonthTotalCost": 24.03
62
+ }
63
+ }
@@ -0,0 +1,30 @@
1
+ import { type Static, type TObject } from '@sinclair/typebox';
2
+ import type { ValueError } from '@sinclair/typebox/compiler';
3
+ export declare class ValidationError extends Error {
4
+ errors: ValueError[];
5
+ readonly isValidationError = true;
6
+ constructor(errors: ValueError[]);
7
+ }
8
+ export declare const validatedFetch: ({ endpoint, apiKey }: {
9
+ apiKey: string;
10
+ endpoint: URL;
11
+ }, fetchImplementation?: typeof fetch) => <Schema extends TObject<import("@sinclair/typebox").TProperties>>(params: {
12
+ resource: string;
13
+ } | {
14
+ resource: string;
15
+ payload: Payload;
16
+ } | {
17
+ resource: string;
18
+ method: string;
19
+ }, schema: Schema) => Promise<{
20
+ error: Error | ValidationError;
21
+ } | {
22
+ result: Static<Schema>;
23
+ }>;
24
+ type Payload = {
25
+ /** The content-type of body */
26
+ type: string;
27
+ body: string;
28
+ };
29
+ export declare const JSONPayload: (payload: Record<string, unknown>) => Payload;
30
+ export {};
@@ -0,0 +1,54 @@
1
+ import {} from '@sinclair/typebox';
2
+ import { slashless } from './slashless.js';
3
+ import { validateWithTypeBox } from '@hello.nrfcloud.com/proto';
4
+ export class ValidationError extends Error {
5
+ errors;
6
+ isValidationError = true;
7
+ constructor(errors) {
8
+ super(`Validation errors`);
9
+ this.name = 'ValidationError';
10
+ this.errors = errors;
11
+ }
12
+ }
13
+ const validate = (SchemaObject, data) => {
14
+ const maybeData = validateWithTypeBox(SchemaObject)(data);
15
+ if ('errors' in maybeData) {
16
+ console.error('Validation failed', { error: maybeData.errors });
17
+ throw new ValidationError(maybeData.errors);
18
+ }
19
+ return maybeData.value;
20
+ };
21
+ const fetchData = (fetchImplementation) => async (...args) => {
22
+ const response = await (fetchImplementation ?? fetch)(...args);
23
+ if (!response.ok)
24
+ throw new Error(`Error fetching status: ${response.status}`);
25
+ return response.json();
26
+ };
27
+ export const validatedFetch = ({ endpoint, apiKey }, fetchImplementation) => async (params, schema) => {
28
+ const { resource } = params;
29
+ const args = {
30
+ headers: headers(apiKey),
31
+ };
32
+ if ('payload' in params) {
33
+ const payload = params.payload;
34
+ args.method = 'POST';
35
+ args.body = payload.body;
36
+ args.headers = { ...(args.headers ?? {}), ['Content-Type']: payload.type };
37
+ }
38
+ else if ('method' in params) {
39
+ args.method = params.method;
40
+ }
41
+ return fetchData(fetchImplementation)(`${slashless(endpoint)}/v1/${resource}`, args)
42
+ .then((res) => ({ result: validate(schema, res) }))
43
+ .catch((error) => ({
44
+ error,
45
+ }));
46
+ };
47
+ const headers = (apiKey) => ({
48
+ Authorization: `Bearer ${apiKey}`,
49
+ Accept: 'application/json; charset=utf-8',
50
+ });
51
+ export const JSONPayload = (payload) => ({
52
+ type: 'application/json',
53
+ body: JSON.stringify(payload),
54
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,101 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Type } from '@sinclair/typebox';
4
+ import { JSONPayload, validatedFetch } from './validatedFetch.js';
5
+ void describe('validatedFetch()', () => {
6
+ void it('should call an nRF Cloud API endpoint and validate the response', async () => {
7
+ const mockFetch = mock.fn(() => ({
8
+ ok: true,
9
+ json: async () => Promise.resolve({
10
+ foo: 'bar',
11
+ }),
12
+ }));
13
+ const vf = validatedFetch({
14
+ endpoint: new URL('https://example.com/'),
15
+ apiKey: 'some-key',
16
+ }, mockFetch);
17
+ const schema = Type.Object({ foo: Type.Literal('bar') });
18
+ const res = await vf({ resource: 'foo' }, schema);
19
+ assert.equal('error' in res, false);
20
+ assert.equal('result' in res, true);
21
+ assert.deepEqual('result' in res && res.result, { foo: 'bar' });
22
+ assert.deepEqual(mockFetch.mock.calls[0]?.arguments, [
23
+ `https://example.com/v1/foo`,
24
+ {
25
+ headers: {
26
+ Accept: 'application/json; charset=utf-8',
27
+ Authorization: 'Bearer some-key',
28
+ },
29
+ },
30
+ ]);
31
+ });
32
+ void it('should return the fetch exception', async () => {
33
+ const err = new Error(`Some error`);
34
+ const vf = validatedFetch({
35
+ endpoint: new URL('https://example.com/'),
36
+ apiKey: 'some-key',
37
+ }, () => Promise.reject(err));
38
+ assert.deepEqual(await vf({ resource: 'some-resource' }, Type.Object({})), {
39
+ error: err,
40
+ });
41
+ });
42
+ void it('should send POST request if body is given', async () => {
43
+ const mockFetch = mock.fn(() => ({
44
+ ok: true,
45
+ json: async () => Promise.resolve({}),
46
+ }));
47
+ const vf = validatedFetch({
48
+ endpoint: new URL('https://example.com/'),
49
+ apiKey: 'some-key',
50
+ }, mockFetch);
51
+ await vf({
52
+ resource: 'foo',
53
+ payload: {
54
+ type: 'application/octet-stream',
55
+ body: 'some data',
56
+ },
57
+ }, Type.Object({}));
58
+ assert.deepEqual(mockFetch.mock.calls[0]?.arguments, [
59
+ `https://example.com/v1/foo`,
60
+ {
61
+ method: 'POST',
62
+ body: 'some data',
63
+ headers: {
64
+ Accept: 'application/json; charset=utf-8',
65
+ Authorization: 'Bearer some-key',
66
+ 'Content-Type': 'application/octet-stream',
67
+ },
68
+ },
69
+ ]);
70
+ });
71
+ void it('should allow to specify the method', async () => {
72
+ const mockFetch = mock.fn(() => ({
73
+ ok: true,
74
+ json: async () => Promise.resolve({}),
75
+ }));
76
+ const vf = validatedFetch({
77
+ endpoint: new URL('https://example.com/'),
78
+ apiKey: 'some-key',
79
+ }, mockFetch);
80
+ await vf({
81
+ resource: 'foo',
82
+ method: 'POST',
83
+ }, Type.Object({}));
84
+ assert.deepEqual(mockFetch.mock.calls[0]?.arguments, [
85
+ `https://example.com/v1/foo`,
86
+ {
87
+ method: 'POST',
88
+ headers: {
89
+ Accept: 'application/json; charset=utf-8',
90
+ Authorization: 'Bearer some-key',
91
+ },
92
+ },
93
+ ]);
94
+ });
95
+ });
96
+ void describe('JSONPayload()', () => {
97
+ void it('should convert a an object to a payload definition to be used in validatedFetch', () => assert.deepEqual(JSONPayload({ foo: 'bar' }), {
98
+ type: 'application/json',
99
+ body: JSON.stringify({ foo: 'bar' }),
100
+ }));
101
+ });
@@ -0,0 +1,5 @@
1
+ import { SSMClient } from '@aws-sdk/client-ssm';
2
+ export declare const getAllAccounts: ({ ssm, stackName, }: {
3
+ ssm: SSMClient;
4
+ stackName: string;
5
+ }) => Promise<string[]>;
@@ -0,0 +1,9 @@
1
+ import { SSMClient } from '@aws-sdk/client-ssm';
2
+ import { Scopes } from './scope.js';
3
+ import { get } from '@bifravst/aws-ssm-settings-helpers';
4
+ export const getAllAccounts = async ({ ssm, stackName, }) => [
5
+ ...new Set(Object.keys(await get(ssm)({
6
+ stackName,
7
+ scope: Scopes.NRFCLOUD_ACCOUNT_PREFIX,
8
+ })()).map((key) => key.split('/')[0])),
9
+ ];
@@ -0,0 +1,4 @@
1
+ export * from './settings.js';
2
+ export * from './scope.js';
3
+ export * from './initializeAccount.js';
4
+ export * from './getAllAccounts.js';
@@ -0,0 +1,4 @@
1
+ export * from './settings.js';
2
+ export * from './scope.js';
3
+ export * from './initializeAccount.js';
4
+ export * from './getAllAccounts.js';
@@ -0,0 +1,10 @@
1
+ import { SSMClient } from '@aws-sdk/client-ssm';
2
+ /**
3
+ * Initializes the nRF Cloud Account
4
+ */
5
+ export declare const initializeAccount: ({ ssm, stackName, account, iotEndpoint, }: {
6
+ ssm: SSMClient;
7
+ stackName: string;
8
+ account: string;
9
+ iotEndpoint: string;
10
+ }) => (reset?: boolean) => Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { SSMClient } from '@aws-sdk/client-ssm';
2
+ import chalk from 'chalk';
3
+ import { nrfCloudAccount } from './scope.js';
4
+ import { get } from '@bifravst/aws-ssm-settings-helpers';
5
+ import { createAccountDevice } from '../api/createAccountDevice.js';
6
+ import { deleteAccountDevice } from '../api/deleteAccountDevice.js';
7
+ import { getAccountInfo } from '../api/getAccountInfo.js';
8
+ import { defaultApiEndpoint, getSettings as getNRFCloudSettings, putSettings as putNRFCloudSettings, } from './settings.js';
9
+ /**
10
+ * Initializes the nRF Cloud Account
11
+ */
12
+ export const initializeAccount = ({ ssm, stackName, account, iotEndpoint, }) => async (reset = false) => {
13
+ const settingsReader = get(ssm)({
14
+ stackName,
15
+ ...nrfCloudAccount(account),
16
+ });
17
+ const { apiKey, apiEndpoint } = await settingsReader();
18
+ if (apiKey === undefined)
19
+ throw new Error(`nRF Cloud API key not configured.`);
20
+ let settingsWithAccountDevice = undefined;
21
+ try {
22
+ settingsWithAccountDevice = await getNRFCloudSettings({
23
+ ssm,
24
+ stackName,
25
+ account,
26
+ })();
27
+ console.log(chalk.white('Stack settings'));
28
+ Object.entries(settingsWithAccountDevice).forEach(([k, v]) => console.log(chalk.yellow(`${k}:`), chalk.blue(v)));
29
+ console.log();
30
+ }
31
+ catch (err) {
32
+ console.log(chalk.magenta(`Stack not configured.`));
33
+ }
34
+ const effectiveEndpoint = apiEndpoint === undefined ? defaultApiEndpoint : new URL(apiEndpoint);
35
+ const accountInfo = await getAccountInfo({
36
+ endpoint: effectiveEndpoint,
37
+ apiKey,
38
+ });
39
+ if ('error' in accountInfo) {
40
+ console.error(chalk.red('⚠️'), '', chalk.red(accountInfo.error.message));
41
+ process.exit(1);
42
+ }
43
+ console.log(chalk.yellow('AWS IoT endpoint:'), chalk.blue(iotEndpoint));
44
+ console.log();
45
+ console.log(chalk.white('nRF Cloud account info:'));
46
+ Object.entries(accountInfo).forEach(([k, v]) => console.log(chalk.yellow(`${k}:`), chalk.blue(v)));
47
+ console.log();
48
+ if (reset === true) {
49
+ console.debug(chalk.magenta(`Deleting account device ...`));
50
+ await deleteAccountDevice({ apiKey, endpoint: effectiveEndpoint });
51
+ console.log(chalk.green(`Account device deleted.`));
52
+ }
53
+ if (settingsWithAccountDevice === undefined || reset === true) {
54
+ console.debug(chalk.magenta(`Generating new account device credentials ...`));
55
+ const { clientCert, privateKey } = await createAccountDevice({
56
+ apiKey,
57
+ endpoint: effectiveEndpoint,
58
+ });
59
+ if (privateKey === undefined) {
60
+ console.error(chalk.red('⚠️'), chalk.red('Account device exists'));
61
+ throw new Error(`Account device exists in account ${accountInfo.team.tenantId}`);
62
+ }
63
+ console.log(chalk.green(`Account device created.`));
64
+ await putNRFCloudSettings({ ssm, stackName, account })({
65
+ accountDeviceClientCert: clientCert,
66
+ accountDevicePrivateKey: privateKey,
67
+ accountDeviceClientId: `account-${accountInfo.team.tenantId}`,
68
+ mqttEndpoint: accountInfo.mqttEndpoint,
69
+ mqttTopicPrefix: accountInfo.mqttTopicPrefix,
70
+ });
71
+ }
72
+ };
@@ -0,0 +1,17 @@
1
+ export declare enum Scopes {
2
+ STACK = "stack",
3
+ NRFCLOUD_BRIDGE_CERTIFICATE = "nRFCloudBridgeCertificate",
4
+ NRFCLOUD_ACCOUNT_PREFIX = "thirdParty"
5
+ }
6
+ export type ScopeContext = {
7
+ scope: string;
8
+ context: string;
9
+ };
10
+ export declare const ScopeContexts: {
11
+ readonly STACK_CONFIG: ScopeContext;
12
+ readonly STACK_MQTT_BRIDGE: ScopeContext;
13
+ readonly STACK_COAP_HEALTH_CHECK: ScopeContext;
14
+ readonly NRFCLOUD_BRIDGE_CERTIFICATE_MQTT: ScopeContext;
15
+ readonly NRFCLOUD_BRIDGE_CERTIFICATE_CA: ScopeContext;
16
+ };
17
+ export declare const nrfCloudAccount: (account: string) => ScopeContext;
@@ -0,0 +1,34 @@
1
+ export var Scopes;
2
+ (function (Scopes) {
3
+ Scopes["STACK"] = "stack";
4
+ Scopes["NRFCLOUD_BRIDGE_CERTIFICATE"] = "nRFCloudBridgeCertificate";
5
+ Scopes["NRFCLOUD_ACCOUNT_PREFIX"] = "thirdParty";
6
+ })(Scopes || (Scopes = {}));
7
+ export const ScopeContexts = {
8
+ STACK_CONFIG: { scope: Scopes.STACK, context: 'context' },
9
+ STACK_MQTT_BRIDGE: {
10
+ scope: Scopes.STACK,
11
+ context: 'mqttBridge',
12
+ },
13
+ STACK_COAP_HEALTH_CHECK: {
14
+ scope: Scopes.STACK,
15
+ context: 'coap-health-check',
16
+ },
17
+ NRFCLOUD_BRIDGE_CERTIFICATE_MQTT: {
18
+ scope: Scopes.NRFCLOUD_BRIDGE_CERTIFICATE,
19
+ context: 'MQTT',
20
+ },
21
+ NRFCLOUD_BRIDGE_CERTIFICATE_CA: {
22
+ scope: Scopes.NRFCLOUD_BRIDGE_CERTIFICATE,
23
+ context: 'CA',
24
+ },
25
+ };
26
+ const nameRx = /^[a-zA-Z0-9_.-]+$/;
27
+ export const nrfCloudAccount = (account) => {
28
+ if (!nameRx.test(account))
29
+ throw new Error(`Invalid account name: ${account}`);
30
+ return {
31
+ scope: Scopes.NRFCLOUD_ACCOUNT_PREFIX,
32
+ context: account,
33
+ };
34
+ };
@@ -0,0 +1,43 @@
1
+ import type { SSMClient } from '@aws-sdk/client-ssm';
2
+ export declare const defaultApiEndpoint: URL;
3
+ export declare const defaultCoAPEndpoint: URL;
4
+ export type Settings = {
5
+ apiEndpoint: URL;
6
+ apiKey: string;
7
+ accountDeviceClientCert: string;
8
+ accountDevicePrivateKey: string;
9
+ accountDeviceClientId: string;
10
+ mqttEndpoint: string;
11
+ mqttTopicPrefix: string;
12
+ coapEndpoint: URL;
13
+ coapPort: number;
14
+ };
15
+ export declare const getSettings: ({ ssm, stackName, account, }: {
16
+ ssm: SSMClient;
17
+ stackName: string;
18
+ account: string;
19
+ }) => (() => Promise<Settings>);
20
+ export declare const getAPISettings: ({ ssm, stackName, account, }: {
21
+ ssm: SSMClient;
22
+ stackName: string;
23
+ account: string;
24
+ }) => (() => Promise<Pick<Settings, 'apiKey' | 'apiEndpoint'>>);
25
+ export declare const putSettings: ({ ssm, stackName, account, }: {
26
+ ssm: SSMClient;
27
+ stackName: string;
28
+ account: string;
29
+ }) => (settings: Partial<Settings>) => Promise<void>;
30
+ export declare const putSetting: ({ ssm, stackName, account, }: {
31
+ ssm: SSMClient;
32
+ stackName: string;
33
+ account: string;
34
+ }) => (property: keyof Settings, value: string, deleteBeforeUpdate: boolean) => Promise<{
35
+ name: string;
36
+ }>;
37
+ export declare const deleteSettings: ({ ssm, stackName, account, }: {
38
+ ssm: SSMClient;
39
+ stackName: string;
40
+ account: string;
41
+ }) => (property: string) => Promise<{
42
+ name: string;
43
+ }>;
@@ -0,0 +1,85 @@
1
+ import { nrfCloudAccount } from './scope.js';
2
+ import { get as getSSMSettings, put as putSSMSettings, remove as deleteSSMSettings, } from '@bifravst/aws-ssm-settings-helpers';
3
+ export const defaultApiEndpoint = new URL('https://api.nrfcloud.com');
4
+ export const defaultCoAPEndpoint = new URL('coaps://coap.nrfcloud.com');
5
+ export const getSettings = ({ ssm, stackName, account, }) => {
6
+ const settingsReader = getSSMSettings(ssm)({
7
+ stackName,
8
+ ...nrfCloudAccount(account),
9
+ });
10
+ return async () => {
11
+ const p = await settingsReader();
12
+ const { apiEndpoint, apiKey, accountDeviceClientCert, accountDevicePrivateKey, mqttEndpoint, accountDeviceClientId, mqttTopicPrefix, coapEndpoint, coapPort, } = p;
13
+ if (apiKey === undefined)
14
+ throw new Error(`No nRF Cloud API key configured!`);
15
+ if (accountDeviceClientCert === undefined)
16
+ throw new Error(`No nRF Cloud account device clientCert configured!`);
17
+ if (accountDevicePrivateKey === undefined)
18
+ throw new Error(`No nRF Cloud account device privateKey configured!`);
19
+ if (accountDeviceClientId === undefined)
20
+ throw new Error(`No nRF Cloud Account Device client ID configured!`);
21
+ if (mqttTopicPrefix === undefined)
22
+ throw new Error(`No nRF Cloud MQTT topic prefix configured!`);
23
+ if (mqttEndpoint === undefined)
24
+ throw new Error(`No nRF Cloud MQTT endpoint configured!`);
25
+ return {
26
+ apiEndpoint: apiEndpoint === undefined ? defaultApiEndpoint : new URL(apiEndpoint),
27
+ apiKey,
28
+ mqttEndpoint,
29
+ accountDeviceClientCert,
30
+ accountDevicePrivateKey,
31
+ accountDeviceClientId,
32
+ mqttTopicPrefix,
33
+ coapEndpoint: coapEndpoint === undefined
34
+ ? defaultCoAPEndpoint
35
+ : new URL(coapEndpoint),
36
+ coapPort: parseInt(coapPort ?? `5684`, 10),
37
+ };
38
+ };
39
+ };
40
+ export const getAPISettings = ({ ssm, stackName, account, }) => {
41
+ const settingsReader = getSSMSettings(ssm)({
42
+ stackName,
43
+ ...nrfCloudAccount(account),
44
+ });
45
+ return async () => {
46
+ const p = await settingsReader();
47
+ const { apiEndpoint, apiKey } = p;
48
+ if (apiKey === undefined)
49
+ throw new Error(`No nRF Cloud API key configured!`);
50
+ return {
51
+ apiEndpoint: apiEndpoint === undefined ? defaultApiEndpoint : new URL(apiEndpoint),
52
+ apiKey,
53
+ };
54
+ };
55
+ };
56
+ export const putSettings = ({ ssm, stackName, account, }) => {
57
+ const settingsWriter = putSSMSettings(ssm)({
58
+ stackName,
59
+ ...nrfCloudAccount(account),
60
+ });
61
+ return async (settings) => {
62
+ await Promise.all(Object.entries(settings).map(async ([k, v]) => settingsWriter({
63
+ property: k,
64
+ value: v.toString(),
65
+ })));
66
+ };
67
+ };
68
+ export const putSetting = ({ ssm, stackName, account, }) => {
69
+ const settingsWriter = putSSMSettings(ssm)({
70
+ stackName,
71
+ ...nrfCloudAccount(account),
72
+ });
73
+ return async (property, value, deleteBeforeUpdate) => settingsWriter({
74
+ property,
75
+ value,
76
+ deleteBeforeUpdate,
77
+ });
78
+ };
79
+ export const deleteSettings = ({ ssm, stackName, account, }) => {
80
+ const settingsDeleter = deleteSSMSettings(ssm)({
81
+ stackName,
82
+ ...nrfCloudAccount(account),
83
+ });
84
+ return async (property) => settingsDeleter({ property });
85
+ };