@camstack/addon-export-alexa 1.2.133 → 1.2.135

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.
@@ -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 }
@@ -94,20 +94,71 @@ function jsonResponse(statusCode, body) {
94
94
  }
95
95
  }
96
96
 
97
- function errorEnvelope(header, type, message) {
97
+ /**
98
+ * `Alexa.ErrorResponse` for a directive. Amazon requires the directive's
99
+ * `correlationToken` and the target `endpoint` on it (alexa-errorresponse);
100
+ * both are copied when the directive carried them.
101
+ */
102
+ function errorEnvelope(header, type, message, endpoint) {
103
+ const endpointId =
104
+ endpoint && typeof endpoint.endpointId === 'string' ? endpoint.endpointId : null
98
105
  return {
99
106
  event: {
100
107
  header: {
101
108
  namespace: 'Alexa',
102
109
  name: 'ErrorResponse',
103
110
  messageId: header.messageId,
111
+ ...(typeof header.correlationToken === 'string'
112
+ ? { correlationToken: header.correlationToken }
113
+ : {}),
104
114
  payloadVersion: '3',
105
115
  },
116
+ ...(endpointId !== null ? { endpoint: { endpointId } } : {}),
106
117
  payload: { type, message },
107
118
  },
108
119
  }
109
120
  }
110
121
 
122
+ /** AcceptGrant's own error: `Alexa.Authorization/ErrorResponse` (alexa-authorization). */
123
+ function acceptGrantErrorEnvelope(header, message) {
124
+ return {
125
+ event: {
126
+ header: {
127
+ namespace: 'Alexa.Authorization',
128
+ name: 'ErrorResponse',
129
+ messageId: header.messageId,
130
+ payloadVersion: '3',
131
+ },
132
+ payload: { type: 'ACCEPT_GRANT_FAILED', message },
133
+ },
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Fill in what an `Alexa.ErrorResponse` from a hub may lack — a hub running an
139
+ * older addon omits `correlationToken` and `endpoint`. Any other body is
140
+ * returned as it is.
141
+ */
142
+ function completeErrorResponse(body, directive) {
143
+ const ev = body && body.event
144
+ if (!ev || !ev.header || ev.header.name !== 'ErrorResponse' || ev.header.namespace !== 'Alexa') {
145
+ return body
146
+ }
147
+ const header = { ...ev.header }
148
+ if (
149
+ header.correlationToken === undefined &&
150
+ typeof directive.header.correlationToken === 'string'
151
+ ) {
152
+ header.correlationToken = directive.header.correlationToken
153
+ }
154
+ const endpointId =
155
+ directive.endpoint && typeof directive.endpoint.endpointId === 'string'
156
+ ? directive.endpoint.endpointId
157
+ : null
158
+ const endpoint = ev.endpoint || (endpointId !== null ? { endpointId } : undefined)
159
+ return { ...body, event: { ...ev, header, ...(endpoint ? { endpoint } : {}) } }
160
+ }
161
+
111
162
  function describe(err) {
112
163
  if (err instanceof Error) return err.message
113
164
  return String(err)
@@ -124,5 +175,7 @@ module.exports = {
124
175
  resolveBrokerPublicUrl,
125
176
  jsonResponse,
126
177
  errorEnvelope,
178
+ acceptGrantErrorEnvelope,
179
+ completeErrorResponse,
127
180
  describe,
128
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-alexa",
3
- "version": "1.2.133",
3
+ "version": "1.2.135",
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",