@camstack/addon-export-alexa 1.2.134 → 1.2.136

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.
@@ -32,18 +32,19 @@ and shows its error on the page.
32
32
 
33
33
  ### The two credential pairs
34
34
 
35
- | Pair | Secrets Manager id | Authenticates | Who else holds it |
35
+ | Pair | SSM parameter (SecureString) | Authenticates | Who else holds it |
36
36
  | --- | --- | --- | --- |
37
- | **LWA "Send Alexa Events"** client (skill console → Permissions → Alexa Skill Messaging) | `camstack/alexa/lwa` | this Lambda → `api.amazon.com` (code exchange, refresh) | nobody |
38
- | **Account-linking** client (skill console → Account Linking) | `camstack/alexa/skill` | Amazon → this Lambda's `/token` | Amazon |
37
+ | **LWA "Send Alexa Events"** client (skill console → Permissions → Alexa Skill Messaging) | `/camstack/alexa/lwa` | this Lambda → `api.amazon.com` (code exchange, refresh) | nobody |
38
+ | **Account-linking** client (skill console → Account Linking) | `/camstack/alexa/skill` | Amazon → this Lambda's `/token` | Amazon |
39
39
 
40
- Each secret is a JSON string:
40
+ Each parameter is a **SecureString** (standard tier, AWS-managed key
41
+ `alias/aws/ssm`) whose value is a JSON string:
41
42
 
42
43
  ```json
43
44
  { "clientId": "…", "clientSecret": "…" }
44
45
  ```
45
46
 
46
- `camstack/alexa/skill` may also carry `"previousClientSecret"` — accepted on
47
+ `/camstack/alexa/skill` may also carry `"previousClientSecret"` — accepted on
47
48
  `/token` while you rotate it (change the value in the skill console, then remove
48
49
  the previous one).
49
50
 
@@ -77,10 +78,18 @@ your skill's locale: NA → `us-east-1`, EU/IN → `eu-west-1`, FE/AU → `us-we
77
78
  call it). Note the URL.
78
79
  3. **Trigger**: **Alexa** → **Alexa Smart Home**, with your skill id
79
80
  (`amzn1.ask.skill.…`). Create the skill first to get it.
80
- 4. **Secrets Manager**: create `camstack/alexa/lwa` and `camstack/alexa/skill`
81
- (JSON above) in the Lambda's region. Enter the values in the console, or
82
- with `--secret-string file://…` from a file you delete afterwards — never as
83
- a command-line argument.
81
+ 4. **SSM Parameter Store** (free standard tier): in the Lambda's region,
82
+ Systems Manager → Parameter Store → **Create parameter**, twice:
83
+ - Name `/camstack/alexa/lwa`, then `/camstack/alexa/skill`
84
+ - Tier **Standard**, Type **SecureString**
85
+ - KMS key source **My current account**, KMS key ID `alias/aws/ssm`
86
+ - Value: the JSON above.
87
+
88
+ Enter the values in the console, or with `aws ssm put-parameter --type
89
+ SecureString --value file://…` from a file you delete afterwards — never as
90
+ a literal command-line argument. The Lambda reads them with `GetParameter`
91
+ and `WithDecryption: true`, caches them for 5 minutes, and re-reads the LWA
92
+ pair at once when LWA answers `invalid_client`.
84
93
  5. **DynamoDB**:
85
94
 
86
95
  ```bash
@@ -102,10 +111,22 @@ your skill's locale: NA → `us-east-1`, EU/IN → `eu-west-1`, FE/AU → `us-we
102
111
  "Version": "2012-10-17",
103
112
  "Statement": [
104
113
  {
105
- "Sid": "ReadSkillSecrets",
114
+ "Sid": "ReadSkillParameters",
106
115
  "Effect": "Allow",
107
- "Action": "secretsmanager:GetSecretValue",
108
- "Resource": "arn:aws:secretsmanager:REGION:ACCOUNT:secret:camstack/alexa/*"
116
+ "Action": "ssm:GetParameter",
117
+ "Resource": "arn:aws:ssm:REGION:ACCOUNT:parameter/camstack/alexa/*"
118
+ },
119
+ {
120
+ "Sid": "DecryptSkillParametersViaSsm",
121
+ "Effect": "Allow",
122
+ "Action": "kms:Decrypt",
123
+ "Resource": "arn:aws:kms:REGION:ACCOUNT:key/AWS_SSM_KEY_ID",
124
+ "Condition": {
125
+ "StringEquals": { "kms:ViaService": "ssm.REGION.amazonaws.com" },
126
+ "StringLike": {
127
+ "kms:EncryptionContext:PARAMETER_ARN": "arn:aws:ssm:REGION:ACCOUNT:parameter/camstack/alexa/*"
128
+ }
129
+ }
109
130
  },
110
131
  {
111
132
  "Sid": "BrokerLinks",
@@ -117,25 +138,32 @@ your skill's locale: NA → `us-east-1`, EU/IN → `eu-west-1`, FE/AU → `us-we
117
138
  }
118
139
  ```
119
140
 
120
- The default `aws/secretsmanager` and `aws/dynamodb` keys need no extra KMS
121
- permission. A customer-managed key would need `kms:Decrypt` on it.
141
+ `AWS_SSM_KEY_ID` is the id of the AWS-managed `alias/aws/ssm` key (KMS
142
+ console → AWS managed keys, or `aws kms describe-key --key-id
143
+ alias/aws/ssm`). AWS's guidance on whether the AWS-managed key needs a
144
+ `kms:Decrypt` grant is inconsistent — its key policy already lets account
145
+ principals decrypt through SSM — so the statement is included, and scoped so
146
+ it can only decrypt these two parameters, and only through SSM. A
147
+ customer-managed key would need the same statement on that key instead. The
148
+ default `aws/dynamodb` key needs no extra permission.
122
149
 
123
150
  7. **Environment variables** (none is a secret):
124
151
 
125
152
  | Name | Value |
126
153
  | --- | --- |
127
- | `LWA_SECRET_ID` | `camstack/alexa/lwa` |
128
- | `SKILL_SECRET_ID` | `camstack/alexa/skill` |
154
+ | `LWA_PARAM` | `/camstack/alexa/lwa` |
155
+ | `SKILL_PARAM` | `/camstack/alexa/skill` |
129
156
  | `TOKENS_TABLE` | `camstack-alexa-tokens`. Unset = broker off: AcceptGrant hands the tokens to the hub as before D753. |
130
157
  | `LAMBDA_REDIRECT_URI` | Function URL + `/token`. AcceptGrant's `redirect_uri`; its origin is the broker URL advertised to hubs. |
131
158
  | `BROKER_PUBLIC_URL` | Optional override of that origin (https only). |
132
159
  | `LEGACY_HUB_FALLBACK` | `0` = a hub without `/oauth/broker-link` (pre-D753 addon) fails AcceptGrant instead of receiving the refresh token. Default on, for the cut-over. |
160
+ | `DEBUG_DIRECTIVES` | `1` = one `directive_trace` log line per Smart Home directive: namespace / name / endpointId / payloadVersion / payload keys asked, and the answered event (ErrorResponse type and message, StateReport property names and value shapes). Never the bearer token or SDP bodies. Diagnostic; default off. |
133
161
  | `ALLOW_MIGRATION` | `0` = refuse `/broker/migrate`. Default on until every hub has migrated. |
134
162
  | `HUB_INGRESS_TOKEN` | Optional defense-in-depth header (`X-CamStack-Ingress`), copied from the addon's **Ingress token** setting. |
135
163
 
136
- Legacy, read only when the matching `*_SECRET_ID` is unset: `LWA_CLIENT_ID`,
164
+ Legacy, read only when the matching `*_PARAM` is unset: `LWA_CLIENT_ID`,
137
165
  `LWA_CLIENT_SECRET`, `SKILL_CLIENT_ID`, `SKILL_CLIENT_SECRET`. Remove them once
138
- the secrets are in Secrets Manager.
166
+ the parameters exist and `*_PARAM` is set.
139
167
 
140
168
  ## Skill manifest
141
169
 
@@ -145,10 +173,10 @@ the secrets are in Secrets Manager.
145
173
  | Account Linking | Authorization grant type | Auth Code Grant |
146
174
  | | Authorization URI | `<FUNCTION_URL>/authorize` |
147
175
  | | Access Token URI | `<FUNCTION_URL>/token` |
148
- | | Client ID / Secret | the `camstack/alexa/skill` values |
176
+ | | Client ID / Secret | the `/camstack/alexa/skill` values |
149
177
  | | Authentication scheme | Credentials in request body (**not** HTTP Basic — the Lambda reads them from the body) |
150
178
  | | Scope, Domain list | empty |
151
- | Permissions | Send Alexa Events | on; its Alexa Client Id/Secret go into `camstack/alexa/lwa` |
179
+ | Permissions | Send Alexa Events | on; its Alexa Client Id/Secret go into `/camstack/alexa/lwa` |
152
180
 
153
181
  ## Cut-over and compatibility
154
182
 
@@ -175,5 +203,5 @@ log line carries a token, code, credential or secret.
175
203
  ## Tests
176
204
 
177
205
  `packages/addon-export-alexa/src/__tests__/lambda-broker.spec.ts` — the whole
178
- Lambda against fake LWA, hub, Secrets Manager and DynamoDB (`npx vitest run`
206
+ Lambda against fake LWA, hub, SSM Parameter Store and DynamoDB (`npx vitest run`
179
207
  in the addon).
@@ -1,37 +1,53 @@
1
1
  /**
2
- * Default AWS wiring. The Lambda Node.js 22 runtime ships AWS SDK v3, so the
3
- * deployment stays a zip of plain files with no node_modules. Required lazily:
4
- * tests inject fakes and never load the SDK.
2
+ * Default AWS wiring. The Lambda Node.js 22 runtime ships AWS SDK v3
3
+ * (`@aws-sdk/client-ssm`, `@aws-sdk/client-dynamodb`), so the deployment stays
4
+ * a zip of plain files with no node_modules. Required lazily: tests inject a
5
+ * fake `loadSdk` and never load the SDK.
5
6
  *
6
7
  * Execution-role permissions this needs (see README → IAM):
7
- * secretsmanager:GetSecretValue on the two camstack/alexa/* secrets
8
- * dynamodb:GetItem/PutItem/DeleteItem on the tokens table
8
+ * ssm:GetParameter on parameter/camstack/alexa/*
9
+ * kms:Decrypt on the `aws/ssm` key, only via SSM and
10
+ * only for those parameters (D761)
11
+ * dynamodb:GetItem/PutItem/DeleteItem on the tokens table
9
12
  */
10
13
  'use strict'
11
14
 
12
- function createAwsClients() {
15
+ /**
16
+ * @param {object} [opts]
17
+ * @param {(name: string) => any} [opts.loadSdk] module loader (tests inject a fake)
18
+ */
19
+ function createAwsClients(opts) {
20
+ const loadSdk = (opts && opts.loadSdk) || require
13
21
  let ddbClient = null
14
- let smClient = null
22
+ let ssmClient = null
23
+ let ssmSdk = null
15
24
 
16
25
  async function send(commandName, input) {
17
- const ddb = require('@aws-sdk/client-dynamodb')
26
+ const ddb = loadSdk('@aws-sdk/client-dynamodb')
18
27
  if (!ddbClient) ddbClient = new ddb.DynamoDBClient({})
19
28
  const Command = ddb[`${commandName}Command`]
20
29
  if (typeof Command !== 'function') throw new Error(`unknown DynamoDB command ${commandName}`)
21
30
  return ddbClient.send(new Command(input))
22
31
  }
23
32
 
24
- async function fetchSecretString(secretId) {
25
- const sm = require('@aws-sdk/client-secrets-manager')
26
- if (!smClient) smClient = new sm.SecretsManagerClient({})
27
- const res = await smClient.send(new sm.GetSecretValueCommand({ SecretId: secretId }))
28
- if (typeof res.SecretString !== 'string') {
29
- throw new Error(`secret ${secretId} has no SecretString`)
33
+ /**
34
+ * The decrypted value of one SecureString parameter. Errors name the
35
+ * parameter, never a value.
36
+ */
37
+ async function readParameter(name) {
38
+ if (!ssmSdk) ssmSdk = loadSdk('@aws-sdk/client-ssm')
39
+ if (!ssmClient) ssmClient = new ssmSdk.SSMClient({})
40
+ const res = await ssmClient.send(
41
+ new ssmSdk.GetParameterCommand({ Name: name, WithDecryption: true }),
42
+ )
43
+ const value = res && res.Parameter ? res.Parameter.Value : undefined
44
+ if (typeof value !== 'string' || value.length === 0) {
45
+ throw new Error(`parameter ${name} has no value`)
30
46
  }
31
- return res.SecretString
47
+ return value
32
48
  }
33
49
 
34
- return { send, fetchSecretString }
50
+ return { send, readParameter }
35
51
  }
36
52
 
37
53
  module.exports = { createAwsClients }
@@ -4,7 +4,7 @@
4
4
  * The skill's LWA client secret authenticates every grant exchange and every
5
5
  * refresh at api.amazon.com. Before D753 a copy of it lived in every hub — in
6
6
  * public source and in a public npm package. Now it lives ONLY here (read from
7
- * Secrets Manager), together with each user's LWA refresh token, and a hub
7
+ * an SSM SecureString parameter), together with each user's LWA refresh token, and a hub
8
8
  * gets exactly what it needs to push events: a short-lived access token.
9
9
  *
10
10
  * A hub authenticates to the broker with a PER-LINK credential
@@ -119,7 +119,7 @@ function lwaErrorCode(res) {
119
119
 
120
120
  /**
121
121
  * One call to the LWA token endpoint with the skill's credentials. On
122
- * `invalid_client` with a Secrets-Manager-sourced secret, drops the cache and
122
+ * `invalid_client` with a Parameter-Store-sourced secret, drops the cache and
123
123
  * retries once with a freshly read value — so a rotation takes effect on the
124
124
  * very next call instead of after the cache TTL.
125
125
  */
@@ -129,7 +129,7 @@ async function lwaTokenCall(deps, params) {
129
129
  throw new BrokerError(500, 'unconfigured', 'LWA client credentials are not configured')
130
130
  }
131
131
  let res = await postLwa(deps, params, creds)
132
- if (lwaErrorCode(res) === 'invalid_client' && creds.source === 'secrets-manager') {
132
+ if (lwaErrorCode(res) === 'invalid_client' && creds.source === 'ssm') {
133
133
  deps.secrets.invalidate('lwa')
134
134
  const fresh = await deps.secrets.lwa()
135
135
  if (fresh && fresh.clientSecret !== creds.clientSecret) {
@@ -0,0 +1,100 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * TEMPORARY diagnostic, on only with `DEBUG_DIRECTIVES=1`: one `directive_trace`
5
+ * line per Smart Home directive — what Alexa asked and what the skill answered.
6
+ *
7
+ * Built from an allow-list of fields, never by dumping the event: the
8
+ * directive's `scope.token` (the account's bearer) and the SDP bodies never
9
+ * reach the log. StateReport property values are summarised (a short scalar,
10
+ * or the object's keys and scalar values) — they are device states, not
11
+ * credentials.
12
+ */
13
+
14
+ const MAX_VALUE_CHARS = 60
15
+
16
+ function isEnabled(env) {
17
+ return Boolean(env) && env.DEBUG_DIRECTIVES === '1'
18
+ }
19
+
20
+ function str(value) {
21
+ return typeof value === 'string' ? value : undefined
22
+ }
23
+
24
+ function summariseValue(value) {
25
+ if (value === null || value === undefined) return String(value)
26
+ if (typeof value !== 'object') return String(value).slice(0, MAX_VALUE_CHARS)
27
+ if (Array.isArray(value)) return `[array:${value.length}]`
28
+ const parts = Object.keys(value).map((k) => {
29
+ const v = value[k]
30
+ return v !== null && typeof v === 'object' ? `${k}:{…}` : `${k}:${String(v)}`
31
+ })
32
+ return `{${parts.join(',')}}`.slice(0, MAX_VALUE_CHARS)
33
+ }
34
+
35
+ function payloadShape(payload) {
36
+ if (!payload || typeof payload !== 'object') return undefined
37
+ return Object.keys(payload)
38
+ .filter((k) => k !== 'scope')
39
+ .map((k) => {
40
+ const v = payload[k]
41
+ if (v && typeof v === 'object' && typeof v.value === 'string') {
42
+ // RTC offers/answers: { format, value: <SDP> } — length only.
43
+ return `${k}(${str(v.format) || 'object'},${v.value.length}b)`
44
+ }
45
+ return k
46
+ })
47
+ }
48
+
49
+ function summariseDirective(event) {
50
+ const d = (event && event.directive) || {}
51
+ const h = d.header || {}
52
+ const ep = d.endpoint || {}
53
+ return {
54
+ namespace: str(h.namespace),
55
+ name: str(h.name),
56
+ endpointId: str(ep.endpointId),
57
+ payloadVersion: str(h.payloadVersion),
58
+ payloadKeys: payloadShape(d.payload),
59
+ }
60
+ }
61
+
62
+ function summariseResponse(body) {
63
+ const ev = body && body.event
64
+ if (!ev || !ev.header) return { kind: 'non-event' }
65
+ const out = {
66
+ namespace: str(ev.header.namespace),
67
+ name: str(ev.header.name),
68
+ endpointId: str(ev.endpoint && ev.endpoint.endpointId),
69
+ }
70
+ const payload = ev.payload || {}
71
+ if (out.name === 'ErrorResponse') {
72
+ out.errorType = str(payload.type)
73
+ out.errorMessage = str(payload.message)
74
+ } else {
75
+ out.payloadKeys = payloadShape(payload)
76
+ }
77
+ const props = body.context && Array.isArray(body.context.properties) ? body.context.properties : null
78
+ if (props) {
79
+ out.properties = props.map(
80
+ (p) => `${str(p && p.namespace)}.${str(p && p.name)}=${summariseValue(p && p.value)}`,
81
+ )
82
+ }
83
+ return out
84
+ }
85
+
86
+ /** Log one trace line; never throws into the directive path. */
87
+ function traceDirective(deps, event, response, startedAt) {
88
+ if (!isEnabled(deps.env)) return
89
+ try {
90
+ deps.log.info('directive_trace', {
91
+ directive: summariseDirective(event),
92
+ response: summariseResponse(response),
93
+ ms: deps.now() - startedAt,
94
+ })
95
+ } catch {
96
+ // diagnostics only
97
+ }
98
+ }
99
+
100
+ module.exports = { traceDirective, summariseDirective, summariseResponse }
@@ -19,10 +19,13 @@
19
19
  * - everything else → forward to the hub
20
20
  *
21
21
  * Configuration (env — none of these is a secret once D753 is deployed):
22
- * LWA_SECRET_ID Secrets Manager id of {"clientId","clientSecret"} for
23
- * the skill's "Send Alexa Events" LWA client.
24
- * SKILL_SECRET_ID Secrets Manager id of {"clientId","clientSecret"
25
- * [,"previousClientSecret"]} for account linking.
22
+ * LWA_PARAM SSM SecureString parameter name (e.g.
23
+ * /camstack/alexa/lwa) holding {"clientId","clientSecret"}
24
+ * for the skill's "Send Alexa Events" LWA client.
25
+ * SKILL_PARAM SSM SecureString parameter name (e.g.
26
+ * /camstack/alexa/skill) holding {"clientId",
27
+ * "clientSecret"[,"previousClientSecret"]} for account
28
+ * linking.
26
29
  * TOKENS_TABLE DynamoDB table of broker links. Unset = broker off
27
30
  * (pre-D753 behaviour: tokens are handed to the hub).
28
31
  * LAMBDA_REDIRECT_URI Function URL + "/token"; the AcceptGrant redirect_uri.
@@ -31,7 +34,9 @@
31
34
  * instead of receiving the refresh token. Default on.
32
35
  * ALLOW_MIGRATION "0" = refuse /broker/migrate. Default on.
33
36
  * HUB_INGRESS_TOKEN Optional defense-in-depth header for hub calls.
34
- * Legacy (read only when the *_SECRET_ID is unset): LWA_CLIENT_ID,
37
+ * DEBUG_DIRECTIVES "1" = log one `directive_trace` line per directive
38
+ * (debug-trace.js; never the bearer). Default off.
39
+ * Legacy (read only when the *_PARAM is unset): LWA_CLIENT_ID,
35
40
  * LWA_CLIENT_SECRET, SKILL_CLIENT_ID, SKILL_CLIENT_SECRET.
36
41
  *
37
42
  * Ships as plain files — no node_modules, no transpile (`zip` the folder's
@@ -50,6 +55,7 @@ const {
50
55
  isBrokerPath,
51
56
  shortId,
52
57
  } = require('./broker.js')
58
+ const { traceDirective } = require('./debug-trace.js')
53
59
  const { timedFetch } = require('./http.js')
54
60
  const { createSecretSource } = require('./secrets.js')
55
61
  const { createDynamoTokenStore } = require('./token-store.js')
@@ -101,7 +107,7 @@ function resolveDeps(overrides) {
101
107
  o.secrets ||
102
108
  createSecretSource({
103
109
  env,
104
- fetchSecretString: (id) => awsClients().fetchSecretString(id),
110
+ readParameter: (name) => awsClients().readParameter(name),
105
111
  }),
106
112
  store,
107
113
  publicUrl: resolveBrokerPublicUrl(env),
@@ -112,23 +118,28 @@ function createHandler(overrides) {
112
118
  let deps = null
113
119
  return async (event) => {
114
120
  if (!deps) deps = resolveDeps(overrides)
121
+ const startedAt = deps.now()
115
122
  try {
116
123
  if (event && event.requestContext && event.requestContext.http) {
117
124
  return await handleLambdaUrl(event, deps)
118
125
  }
119
126
  if (event && event.directive && event.directive.header) {
120
- return await handleDirective(event, deps)
127
+ const res = await handleDirective(event, deps)
128
+ traceDirective(deps, event, res, startedAt)
129
+ return res
121
130
  }
122
131
  return jsonResponse(400, { error: 'unknown_event' })
123
132
  } catch (err) {
124
133
  deps.log.error('camstack-alexa: unhandled error', describe(err))
125
134
  if (event && event.directive && event.directive.header) {
126
- return errorEnvelope(
135
+ const res = errorEnvelope(
127
136
  event.directive.header,
128
137
  'INTERNAL_ERROR',
129
138
  describe(err),
130
139
  event.directive.endpoint,
131
140
  )
141
+ traceDirective(deps, event, res, startedAt)
142
+ return res
132
143
  }
133
144
  return jsonResponse(500, { error: 'internal_error', message: describe(err) })
134
145
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Skill credentials, read where they live: AWS Secrets Manager (D753).
2
+ * Skill credentials, read where they live: SSM Parameter Store SecureString
3
+ * parameters, decrypted with the AWS-managed `aws/ssm` key (D753, D761).
3
4
  *
4
5
  * Two credential pairs exist, and they authenticate DIFFERENT parties:
5
6
  *
@@ -11,30 +12,32 @@
11
12
  * Account Linking page. Authenticates AMAZON to this Lambda's
12
13
  * `/token` route. Amazon holds a copy; nothing else needs one.
13
14
  *
14
- * Each secret is a JSON string `{"clientId": "...", "clientSecret": "..."}`.
15
+ * Each parameter's value is a JSON string
16
+ * `{"clientId": "...", "clientSecret": "..."}`.
15
17
  * `skill` may also carry `previousClientSecret`, accepted on `/token` while a
16
18
  * rotation is in flight (change Amazon's copy, then drop the previous one).
17
19
  *
18
20
  * Resolution, per pair:
19
- * 1. `<KIND>_SECRET_ID` set → Secrets Manager, cached for `ttlMs`.
20
- * 2. otherwise → the legacy plain env vars
21
- * (`LWA_CLIENT_ID`/`LWA_CLIENT_SECRET`,
22
- * `SKILL_CLIENT_ID`/`SKILL_CLIENT_SECRET`), so
23
- * this code can be deployed before the secrets
24
- * exist and the cut-over has no downtime.
21
+ * 1. `<KIND>_PARAM` set → that SSM parameter (read with decryption),
22
+ * cached for `ttlMs`.
23
+ * 2. otherwise → the legacy plain env vars
24
+ * (`LWA_CLIENT_ID`/`LWA_CLIENT_SECRET`,
25
+ * `SKILL_CLIENT_ID`/`SKILL_CLIENT_SECRET`), so this
26
+ * code can be deployed before the parameters exist
27
+ * and the cut-over has no downtime.
25
28
  *
26
29
  * The cache is what makes a rotation cheap: a new value is picked up within
27
30
  * `ttlMs`, and immediately when LWA answers `invalid_client` (the caller calls
28
- * `invalidate`). Error messages name the secret id, never a value.
31
+ * `invalidate`). Error messages name the parameter, never a value.
29
32
  */
30
33
  'use strict'
31
34
 
32
35
  const DEFAULT_SECRET_TTL_MS = 5 * 60 * 1000
33
36
 
34
37
  const KINDS = {
35
- lwa: { secretIdVar: 'LWA_SECRET_ID', idVar: 'LWA_CLIENT_ID', secretVar: 'LWA_CLIENT_SECRET' },
38
+ lwa: { paramVar: 'LWA_PARAM', idVar: 'LWA_CLIENT_ID', secretVar: 'LWA_CLIENT_SECRET' },
36
39
  skill: {
37
- secretIdVar: 'SKILL_SECRET_ID',
40
+ paramVar: 'SKILL_PARAM',
38
41
  idVar: 'SKILL_CLIENT_ID',
39
42
  secretVar: 'SKILL_CLIENT_SECRET',
40
43
  },
@@ -43,7 +46,7 @@ const KINDS = {
43
46
  /**
44
47
  * @param {object} opts
45
48
  * @param {Record<string, string|undefined>} opts.env
46
- * @param {(secretId: string) => Promise<string>} opts.fetchSecretString
49
+ * @param {(name: string) => Promise<string>} opts.readParameter decrypted value
47
50
  * @param {() => number} [opts.now]
48
51
  * @param {number} [opts.ttlMs]
49
52
  */
@@ -55,12 +58,12 @@ function createSecretSource(opts) {
55
58
 
56
59
  async function load(kind) {
57
60
  const spec = KINDS[kind]
58
- const secretId = env[spec.secretIdVar]
59
- if (secretId) {
61
+ const paramName = env[spec.paramVar]
62
+ if (paramName) {
60
63
  const hit = cache.get(kind)
61
64
  if (hit && now() - hit.at < ttlMs) return hit.value
62
- const raw = await opts.fetchSecretString(secretId)
63
- const value = parseCredentialSecret(raw, secretId)
65
+ const raw = await opts.readParameter(paramName)
66
+ const value = parseCredentialParameter(raw, paramName)
64
67
  cache.set(kind, { value, at: now() })
65
68
  return value
66
69
  }
@@ -79,12 +82,12 @@ function createSecretSource(opts) {
79
82
  }
80
83
  }
81
84
 
82
- function parseCredentialSecret(raw, secretId) {
85
+ function parseCredentialParameter(raw, paramName) {
83
86
  let obj
84
87
  try {
85
88
  obj = JSON.parse(raw)
86
89
  } catch {
87
- throw new Error(`secret ${secretId} is not a JSON object`)
90
+ throw new Error(`parameter ${paramName} is not a JSON object`)
88
91
  }
89
92
  if (
90
93
  !obj ||
@@ -93,13 +96,13 @@ function parseCredentialSecret(raw, secretId) {
93
96
  typeof obj.clientSecret !== 'string' ||
94
97
  obj.clientSecret.length === 0
95
98
  ) {
96
- throw new Error(`secret ${secretId} must be {"clientId": "...", "clientSecret": "..."}`)
99
+ throw new Error(`parameter ${paramName} must be {"clientId": "...", "clientSecret": "..."}`)
97
100
  }
98
- const out = { clientId: obj.clientId, clientSecret: obj.clientSecret, source: 'secrets-manager' }
101
+ const out = { clientId: obj.clientId, clientSecret: obj.clientSecret, source: 'ssm' }
99
102
  if (typeof obj.previousClientSecret === 'string' && obj.previousClientSecret.length > 0) {
100
103
  out.previousClientSecret = obj.previousClientSecret
101
104
  }
102
105
  return out
103
106
  }
104
107
 
105
- module.exports = { createSecretSource, parseCredentialSecret, DEFAULT_SECRET_TTL_MS }
108
+ module.exports = { createSecretSource, parseCredentialParameter, DEFAULT_SECRET_TTL_MS }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-alexa",
3
- "version": "1.2.134",
3
+ "version": "1.2.136",
4
4
  "description": "Alexa Smart Home Skill export — hub-side OAuth provider + directive handler. The companion AWS Lambda forwards Alexa directives to this addon's /addon/export-alexa/directive endpoint.",
5
5
  "keywords": [
6
6
  "camstack",