Skip to content

Commit 0357ed9

Browse files
committed
feat: add support for "unsupported" devices
1 parent b96f54d commit 0357ed9

File tree

11 files changed

+296
-74
lines changed

11 files changed

+296
-74
lines changed

cli/commands/configure-device.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { apiClient, DeviceConfig } from '../../nrfcloud/apiClient.js'
88
import { getAPISettings } from '../../nrfcloud/settings.js'
99
import type { CommandDefinition } from './CommandDefinition.js'
1010
import type { Static } from '@sinclair/typebox'
11+
import { UNSUPPORTED_MODEL } from '../../devices/registerUnsupportedDevice.js'
1112

1213
const defaultActiveWaitTimeSeconds = 120
1314
const defaultLocationTimeoutSeconds = 60
@@ -58,6 +59,24 @@ export const configureDeviceCommand = ({
5859

5960
console.log(chalk.yellow('ID'), chalk.blue(device.id))
6061

62+
if (device.model === UNSUPPORTED_MODEL) {
63+
console.error(
64+
chalk.red(chalk.red('⚠️'), '', `Device is marked as unsupported.`),
65+
)
66+
process.exit(1)
67+
}
68+
69+
if (device.account === undefined) {
70+
console.error(
71+
chalk.red(
72+
chalk.red('⚠️'),
73+
'',
74+
`Device is not associated with an account.`,
75+
),
76+
)
77+
process.exit(1)
78+
}
79+
6180
const { apiKey, apiEndpoint } = await getAPISettings({
6281
ssm,
6382
stackName,

cli/commands/import-unsupported-device.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type DynamoDBClient } from '@aws-sdk/client-dynamodb'
22
import { isFingerprint } from '@hello.nrfcloud.com/proto/fingerprint'
33
import chalk from 'chalk'
4-
import { markFingerprintAsUnsupported } from '../../devices/markFingerprintAsUnsupported.js'
4+
import { registerUnsupportedDevice } from '../../devices/registerUnsupportedDevice.js'
55
import type { CommandDefinition } from './CommandDefinition.js'
66
import { isIMEI } from './import-devices.js'
77
import { readFile } from 'node:fs/promises'
@@ -43,12 +43,12 @@ export const importUnsupportedDevice = ({
4343
process.exit(1)
4444
}
4545

46-
const res = await markFingerprintAsUnsupported({
46+
const res = await registerUnsupportedDevice({
4747
db,
4848
devicesTableName,
4949
})({
5050
fingerprint,
51-
deviceId,
51+
id: deviceId,
5252
})
5353
if ('error' in res) {
5454
console.error(chalk.red(`Failed to store fingerprint!`))

cli/commands/show-device.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getDevice } from '../../devices/getDevice.js'
66
import { apiClient } from '../../nrfcloud/apiClient.js'
77
import { getAPISettings } from '../../nrfcloud/settings.js'
88
import type { CommandDefinition } from './CommandDefinition.js'
9+
import { UNSUPPORTED_MODEL } from '../../devices/registerUnsupportedDevice.js'
910

1011
export const showDeviceCommand = ({
1112
ssm,
@@ -37,6 +38,20 @@ export const showDeviceCommand = ({
3738

3839
const { device } = maybeDevice
3940

41+
if (device.model === UNSUPPORTED_MODEL || device.account === undefined) {
42+
console.log(
43+
table([
44+
['Fingerprint', 'Device ID', 'Model'],
45+
[
46+
chalk.green(device.fingerprint),
47+
chalk.blue(device.id),
48+
chalk.magenta(device.model),
49+
],
50+
]),
51+
)
52+
return
53+
}
54+
4055
const { apiKey, apiEndpoint } = await getAPISettings({
4156
ssm,
4257
stackName,

devices/getDevice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const getDevice =
2121
id: string
2222
fingerprint: string
2323
model: string
24-
account: string
24+
account?: string
2525
}
2626
}
2727
| { error: Error }
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { PutItemCommand, type DynamoDBClient } from '@aws-sdk/client-dynamodb'
2+
import { marshall } from '@aws-sdk/util-dynamodb'
3+
4+
export const UNSUPPORTED_MODEL = 'unsupported'
5+
6+
export const registerUnsupportedDevice =
7+
({
8+
db,
9+
devicesTableName,
10+
}: {
11+
db: DynamoDBClient
12+
devicesTableName: string
13+
}) =>
14+
async ({
15+
fingerprint,
16+
id,
17+
}: {
18+
fingerprint: string
19+
id: string
20+
}): Promise<{ success: true } | { error: Error }> => {
21+
try {
22+
await db.send(
23+
new PutItemCommand({
24+
TableName: devicesTableName,
25+
Item: marshall({
26+
fingerprint,
27+
deviceId: id,
28+
model: UNSUPPORTED_MODEL,
29+
}),
30+
ConditionExpression: 'attribute_not_exists(fingerprint)',
31+
}),
32+
)
33+
return { success: true }
34+
} catch (error) {
35+
return { error: error as Error }
36+
}
37+
}

feature-runner/steps/device.ts

Lines changed: 103 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getDevice as getDeviceFromIndex } from '../../devices/getDevice.js'
1515
import { getAttributesForDevice } from '../../devices/getAttributesForDevice.js'
1616
import { registerDevice } from '../../devices/registerDevice.js'
1717
import type { getAllAccountsSettings } from '../../nrfcloud/allAccounts.js'
18+
import { registerUnsupportedDevice } from '../../devices/registerUnsupportedDevice.js'
1819

1920
const createDeviceForModel = ({
2021
db,
@@ -55,42 +56,108 @@ const createDeviceForModel = ({
5556
account,
5657
})
5758

58-
await pRetry(
59-
async () => {
60-
const res = await getDeviceFromIndex({
61-
db,
62-
devicesTableName: devicesTable,
63-
devicesIndexName: devicesTableFingerprintIndexName,
64-
})({ fingerprint })
65-
if ('error' in res)
66-
throw new Error(`Failed to resolve fingerprint ${fingerprint}!`)
67-
},
68-
{
69-
retries: 5,
70-
minTimeout: 500,
71-
maxTimeout: 1000,
72-
},
73-
)
59+
await waitForDeviceToBeAvailable({
60+
db,
61+
devicesTable,
62+
devicesTableFingerprintIndexName,
63+
})(fingerprint)
64+
await waitForDeviceAttributesToBeAvailable({
65+
db,
66+
devicesTable,
67+
})(id)
68+
69+
context[storageName] = fingerprint
70+
context[`${storageName}_deviceId`] = id
71+
progress(`Device registered: ${fingerprint} (${id})`)
72+
},
73+
)
74+
75+
const waitForDeviceToBeAvailable =
76+
({
77+
db,
78+
devicesTable,
79+
devicesTableFingerprintIndexName,
80+
}: {
81+
db: DynamoDBClient
82+
devicesTable: string
83+
devicesTableFingerprintIndexName: string
84+
}) =>
85+
async (fingerprint: string) => {
86+
await pRetry(
87+
async () => {
88+
const res = await getDeviceFromIndex({
89+
db,
90+
devicesTableName: devicesTable,
91+
devicesIndexName: devicesTableFingerprintIndexName,
92+
})({ fingerprint })
93+
if ('error' in res)
94+
throw new Error(`Failed to resolve fingerprint ${fingerprint}!`)
95+
},
96+
{
97+
retries: 5,
98+
minTimeout: 500,
99+
maxTimeout: 1000,
100+
},
101+
)
102+
}
74103

75-
await pRetry(
76-
async () => {
77-
const res = await getAttributesForDevice({
78-
db,
79-
DevicesTableName: devicesTable,
80-
})(id)
81-
if ('error' in res)
82-
throw new Error(`Failed to get model for device ${id}!`)
83-
},
84-
{
85-
retries: 5,
86-
minTimeout: 500,
87-
maxTimeout: 1000,
88-
},
104+
const waitForDeviceAttributesToBeAvailable =
105+
({ db, devicesTable }: { db: DynamoDBClient; devicesTable: string }) =>
106+
async (id: string) => {
107+
await pRetry(
108+
async () => {
109+
const res = await getAttributesForDevice({
110+
db,
111+
DevicesTableName: devicesTable,
112+
})(id)
113+
if ('error' in res)
114+
throw new Error(`Failed to get model for device ${id}!`)
115+
},
116+
{
117+
retries: 5,
118+
minTimeout: 500,
119+
maxTimeout: 1000,
120+
},
121+
)
122+
}
123+
124+
const createUnsupportedDevice = ({
125+
db,
126+
devicesTable,
127+
devicesTableFingerprintIndexName,
128+
}: {
129+
db: DynamoDBClient
130+
131+
devicesTable: string
132+
devicesTableFingerprintIndexName: string
133+
}) =>
134+
regExpMatchedStep(
135+
{
136+
regExp:
137+
/^I have the fingerprint for an unsupported device in `(?<storageName>[^`]+)`$/,
138+
schema: Type.Object({
139+
storageName: Type.String(),
140+
}),
141+
},
142+
async ({ match: { storageName }, log: { progress }, context }) => {
143+
const fingerprint = `92b.${generateCode()}`
144+
const id = randomUUID()
145+
146+
progress(
147+
`Registering unsupported device ${id} into table ${devicesTable}`,
89148
)
149+
await registerUnsupportedDevice({ db, devicesTableName: devicesTable })({
150+
id,
151+
fingerprint,
152+
})
153+
await waitForDeviceToBeAvailable({
154+
db,
155+
devicesTable,
156+
devicesTableFingerprintIndexName,
157+
})(fingerprint)
90158

91159
context[storageName] = fingerprint
92160
context[`${storageName}_deviceId`] = id
93-
94161
progress(`Device registered: ${fingerprint} (${id})`)
95162
},
96163
)
@@ -164,5 +231,10 @@ export const steps = (
164231
}: { devicesTableFingerprintIndexName: string; devicesTable: string },
165232
): StepRunner<Record<string, string>>[] => [
166233
createDeviceForModel({ db, devicesTableFingerprintIndexName, devicesTable }),
234+
createUnsupportedDevice({
235+
db,
236+
devicesTableFingerprintIndexName,
237+
devicesTable,
238+
}),
167239
publishDeviceMessage(allAccountSettings),
168240
]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
exampleContext:
3+
fingerprintUnsupported: 922.s5ahm3
4+
fingerprintUnsupported_deviceId: b137abdd-618b-4ab3-a2f1-14b66bc96738
5+
---
6+
7+
# Unsupported Device
8+
9+
> Some devices are shipped with a QR code on the device but are not intended to
10+
> be used on hello.nrfcloud.com. In case a user scans such a QR code, they
11+
> should receive a useful error message.
12+
13+
## Background
14+
15+
Given I have the fingerprint for an unsupported device in
16+
`fingerprintUnsupported`
17+
18+
## Connect with the fingerprint of an unsupported device
19+
20+
When I connect to the websocket using fingerprint `${fingerprintUnsupported}`
21+
22+
Soon I should receive a message on the websocket that matches
23+
24+
```json
25+
{
26+
"@context": "https://github.com/hello-nrfcloud/proto/deviceIdentity",
27+
"id": "${fingerprintUnsupported_deviceId}",
28+
"model": "unsupported"
29+
}
30+
```

lambda/authorizer.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { PolicyDocument } from 'aws-lambda'
1111
import { metricsForComponent } from './metrics/metrics.js'
1212
import { logger } from './util/logger.js'
1313
import type { WebsocketConnectionContext } from './ws/AuthorizedEvent.js'
14+
import { UNSUPPORTED_MODEL } from '../devices/registerUnsupportedDevice.js'
1415

1516
const { DevicesTableName, DevicesIndexName } = fromEnv({
1617
DevicesTableName: 'DEVICES_TABLE_NAME',
@@ -83,7 +84,7 @@ const h = async (event: {
8384

8485
const { model, deviceId, account } = device
8586

86-
if (model === undefined || deviceId === undefined || account === undefined) {
87+
if (model === undefined || deviceId === undefined) {
8788
log.error(`Required information is missing`, {
8889
fingerprint,
8990
model,
@@ -94,6 +95,17 @@ const h = async (event: {
9495
return deny
9596
}
9697

98+
if (model !== UNSUPPORTED_MODEL && account === undefined) {
99+
log.error(`Account is missing`, {
100+
fingerprint,
101+
model,
102+
deviceId,
103+
account,
104+
})
105+
track('authorizer:badInfo', MetricUnits.Count, 1)
106+
return deny
107+
}
108+
97109
// Track usage of fingerprint
98110
const now = new Date()
99111
void db.send(

0 commit comments

Comments
 (0)