@fusebit/add-on-sdk
Advanced tools
Comparing version 0.0.1 to 1.0.2
289
lib/index.js
@@ -8,3 +8,3 @@ const Superagent = require('superagent'); | ||
} | ||
}; | ||
} | ||
@@ -14,3 +14,3 @@ function validateReturnTo(ctx) { | ||
const validReturnTo = (ctx.configuration.fusebit_allowed_return_to || '').split(','); | ||
const match = validReturnTo.find(allowed => { | ||
const match = validReturnTo.find((allowed) => { | ||
if (allowed === ctx.query.returnTo) { | ||
@@ -25,7 +25,5 @@ return true; | ||
if (!match) { | ||
throw { | ||
status: 403, | ||
message: `The specified 'returnTo' URL '${ctx.query.returnTo}' does not match any of the allowed returnTo URLs of the '${ | ||
ctx.boundaryId}/${ctx.functionId}' Fusebit Add-On component. If this is a valid request, add the specified 'returnTo' URL to the 'fusebit_allowed_return_to' configuration property of the '${ | ||
ctx.boundaryId}/${ctx.functionId}' Fusebit Add-On component.` | ||
throw { | ||
status: 403, | ||
message: `The specified 'returnTo' URL '${ctx.query.returnTo}' does not match any of the allowed returnTo URLs of the '${ctx.boundaryId}/${ctx.functionId}' Fusebit Add-On component. If this is a valid request, add the specified 'returnTo' URL to the 'fusebit_allowed_return_to' configuration property of the '${ctx.boundaryId}/${ctx.functionId}' Fusebit Add-On component.`, | ||
}; | ||
@@ -40,3 +38,3 @@ } | ||
const { states, initialState } = configure; | ||
return async (ctx) => { | ||
return async (ctx) => { | ||
if (!disableDebug) { | ||
@@ -53,3 +51,3 @@ debug('DEBUGGING ENABLED. To disable debugging information, comment out the `debug` configuration setting.'); | ||
if (ctx.query.status === 'error') { | ||
// This is a callback from a subordinate service that resulted in an error; propagate | ||
// This is a callback from a subordinate service that resulted in an error; propagate | ||
throw { status: data.status || 500, message: data.message || 'Unspecified error', state }; | ||
@@ -60,11 +58,9 @@ } | ||
return await stateHandler(ctx, state, data); | ||
} | ||
else { | ||
} else { | ||
throw { status: 400, message: `Unsupported configuration state '${state.configurationState}'`, state }; | ||
} | ||
} | ||
catch (e) { | ||
} catch (e) { | ||
return exports.completeWithError(ctx, e); | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -79,4 +75,4 @@ | ||
let lastSegment; | ||
do { | ||
lastSegment = pathSegments.pop(); | ||
do { | ||
lastSegment = pathSegments.pop(); | ||
} while (!lastSegment && pathSegments.length > 0); | ||
@@ -91,7 +87,6 @@ try { | ||
return await settingsManager(ctx); | ||
} | ||
else { | ||
} else { | ||
// There is no configuration stage, simply redirect back to the caller with success | ||
validateReturnTo(ctx); | ||
let [state, data] = exports.getInputs(ctx, configure && configure.initialState || 'none'); | ||
let [state, data] = exports.getInputs(ctx, (configure && configure.initialState) || 'none'); | ||
return exports.completeWithSuccess(state, data); | ||
@@ -112,9 +107,8 @@ } | ||
throw { status: 404, message: 'Not found' }; | ||
}; | ||
} | ||
catch (e) { | ||
} | ||
} catch (e) { | ||
return exports.completeWithError(ctx, e); | ||
} | ||
}; | ||
} | ||
}; | ||
@@ -129,4 +123,3 @@ exports.serializeState = (state) => Buffer.from(JSON.stringify(state)).toString('base64'); | ||
data = ctx.query.data ? exports.deserializeState(ctx.query.data) : {}; | ||
} | ||
catch (e) { | ||
} catch (e) { | ||
throw { status: 400, message: `Malformed 'data' parameter` }; | ||
@@ -137,5 +130,8 @@ } | ||
if (!initialConfigurationState) { | ||
throw { status: 400, message: `State consistency error. Initial configuration state is not specified, and 'state' parameter is missing.` }; | ||
throw { | ||
status: 400, | ||
message: `State consistency error. Initial configuration state is not specified, and 'state' parameter is missing.`, | ||
}; | ||
} | ||
['baseUrl', 'accountId', 'subscriptionId', 'boundaryId','functionId', 'templateName'].forEach(p => { | ||
['baseUrl', 'accountId', 'subscriptionId', 'boundaryId', 'functionId', 'templateName'].forEach((p) => { | ||
if (!data[p]) { | ||
@@ -145,10 +141,15 @@ throw { status: 400, message: `Missing 'data.${p}' input parameter`, state: ctx.query.state }; | ||
}); | ||
return [{ configurationState: initialConfigurationState, returnTo: ctx.query.returnTo, returnToState: ctx.query.state}, data]; | ||
} | ||
else if (ctx.query.state) { | ||
return [ | ||
{ | ||
configurationState: initialConfigurationState, | ||
returnTo: ctx.query.returnTo, | ||
returnToState: ctx.query.state, | ||
}, | ||
data, | ||
]; | ||
} else if (ctx.query.state) { | ||
// Continuation of the add-on component interaction (e.g. form post from a settings manager) | ||
try { | ||
return [JSON.parse(Buffer.from(ctx.query.state, 'base64').toString()), data]; | ||
} | ||
catch (e) { | ||
} catch (e) { | ||
throw { status: 400, message: `Malformed 'state' parameter` }; | ||
@@ -159,9 +160,9 @@ } | ||
} | ||
} | ||
}; | ||
exports.completeWithSuccess = (state, data) => { | ||
const location = `${state.returnTo}?status=success&data=${ | ||
encodeURIComponent(exports.serializeState(data)) | ||
}` + (state.returnToState ? `&state=${encodeURIComponent(state.returnToState)}` : ''); | ||
return { status: 302, headers: { location }}; | ||
const location = | ||
`${state.returnTo}?status=success&data=${encodeURIComponent(exports.serializeState(data))}` + | ||
(state.returnToState ? `&state=${encodeURIComponent(state.returnToState)}` : ''); | ||
return { status: 302, headers: { location } }; | ||
}; | ||
@@ -171,12 +172,11 @@ | ||
debug('COMPLETE WITH ERROR', error); | ||
let returnTo = error.state && error.state.returnTo || ctx.query.returnTo; | ||
let state = error.state && error.state.returnToState || (ctx.query.returnTo && ctx.query.state); | ||
let returnTo = (error.state && error.state.returnTo) || ctx.query.returnTo; | ||
let state = (error.state && error.state.returnToState) || (ctx.query.returnTo && ctx.query.state); | ||
let body = { status: error.status || 500, message: error.message }; | ||
if (returnTo) { | ||
const location = `${returnTo}?status=error&data=${ | ||
encodeURIComponent(exports.serializeState(body)) | ||
}` + (state ? `&state=${encodeURIComponent(state)}` : ''); | ||
return { status: 302, headers: { location }}; | ||
} | ||
else { | ||
const location = | ||
`${returnTo}?status=error&data=${encodeURIComponent(exports.serializeState(body))}` + | ||
(state ? `&state=${encodeURIComponent(state)}` : ''); | ||
return { status: 302, headers: { location } }; | ||
} else { | ||
return { status: body.status, body }; | ||
@@ -187,54 +187,157 @@ } | ||
exports.getSelfUrl = (ctx) => { | ||
const baseUrl = ctx.headers['x-forwarded-proto'] | ||
? `${ctx.headers['x-forwarded-proto'].split(',')[0]}://${ctx.headers.host}` | ||
: `${ctx.protocol}://${ctx.headers.host}`; | ||
return `${baseUrl}/v1/run/${ctx.subscriptionId}/${ctx.boundaryId}/${ctx.functionId}`; | ||
const baseUrl = ctx.headers['x-forwarded-proto'] | ||
? `${ctx.headers['x-forwarded-proto'].split(',')[0]}://${ctx.headers.host}` | ||
: `${ctx.protocol}://${ctx.headers.host}`; | ||
return `${baseUrl}/v1/run/${ctx.subscriptionId}/${ctx.boundaryId}/${ctx.functionId}`; | ||
}; | ||
exports.redirect = (ctx, state, data, redirectUrl, nextConfigurationState) => { | ||
state.configurationState = nextConfigurationState; | ||
const location = `${ | ||
redirectUrl | ||
}?returnTo=${ | ||
`${exports.getSelfUrl(ctx)}/configure` | ||
}&state=${ | ||
encodeURIComponent(exports.serializeState(state)) | ||
}&data=${ | ||
encodeURIComponent(exports.serializeState(data)) | ||
}`; | ||
const location = `${redirectUrl}?returnTo=${`${exports.getSelfUrl(ctx)}/configure`}&state=${encodeURIComponent( | ||
exports.serializeState(state) | ||
)}&data=${encodeURIComponent(exports.serializeState(data))}`; | ||
return { status: 302, headers: { location } }; | ||
} | ||
}; | ||
exports.createStorage = async (ctx) => { | ||
let issuerCreated = false; | ||
let issuerId; | ||
let clientId; | ||
try { | ||
// Create a PKI issuer to represent the the Add-on Handler | ||
issuerId = `uri:fusebit-template:addon-${ctx.body.functionId}:${ctx.body.subscriptionId}:${ctx.body.boundaryId}:${ctx.body.functionId}`; | ||
console.log(`Creating the storage keys: ${issuerId}`); | ||
const keyId = 'key-1'; | ||
const { publicKey, privateKey } = await new Promise((resolve, reject) => | ||
require('crypto').generateKeyPair( | ||
'rsa', | ||
{ | ||
modulusLength: 512, | ||
publicKeyEncoding: { format: 'pem', type: 'spki' }, | ||
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, | ||
}, | ||
(error, publicKey, privateKey) => (error ? reject(error) : resolve({ publicKey, privateKey })) | ||
) | ||
); | ||
console.log('Creating the issuer'); | ||
await Superagent.post(`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/issuer/${encodeURIComponent(issuerId)}`) | ||
.set('Authorization', ctx.headers['authorization']) // pass-through authorization | ||
.send({ | ||
displayName: `Issuer for add-on handler ${ctx.body.subscriptionId}/${ctx.body.boundaryId}/${ctx.body.functionId}`, | ||
publicKeys: [{ keyId, publicKey }], | ||
}); | ||
issuerCreated = true; | ||
console.log('ISSUER CREATED'); | ||
// Create a Client for the add-on handler with permissions to storage | ||
const subject = 'client-1'; | ||
const storageId = uuid.v4(); | ||
console.log(`Creating the storage client: ${storageId}`); | ||
clientId = ( | ||
await Superagent.post(`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/client`) | ||
.set('Authorization', ctx.headers['authorization']) // pass-through authorization | ||
.send({ | ||
displayName: `Client for add-on handler ${ctx.body.subscriptionId}/${ctx.body.boundaryId}/${ctx.body.functionId}`, | ||
identities: [{ issuerId, subject }], | ||
access: { | ||
allow: [ | ||
{ | ||
action: 'storage:*', | ||
resource: `/account/${ctx.body.accountId}/subscription/${ctx.body.subscriptionId}/storage/${storageId}/`, | ||
}, | ||
], | ||
}, | ||
}) | ||
).body.id; | ||
console.log('Storage successfully created'); | ||
// Return the appropriate configuration elements for a consumer. | ||
return { | ||
fusebit_storage_key: Buffer.from(privateKey).toString('base64'), | ||
fusebit_storage_key_id: keyId, | ||
fusebit_storage_issuer_id: issuerId, | ||
fusebit_storage_subject: subject, | ||
fusebit_storage_id: storageId, | ||
fusebit_storage_audience: ctx.body.baseUrl, | ||
fusebit_storage_account_id: ctx.body.accountId, | ||
fusebit_storage_subscription_id: ctx.body.subscriptionId, | ||
}; | ||
} catch (e) { | ||
if (clientId) { | ||
try { | ||
await Superagent.delete(`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/client/${clientId}`).set( | ||
'Authorization', | ||
ctx.headers['authorization'] | ||
); // pass-through authorization | ||
} catch (_) {} | ||
} | ||
if (issuerCreated) { | ||
try { | ||
await Superagent.delete(`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/issuer/${encodeURIComponent(issuerId)}`).set( | ||
'Authorization', | ||
ctx.headers['authorization'] | ||
); // pass-through authorization | ||
} catch (_) {} | ||
} | ||
throw e; | ||
} | ||
}; | ||
exports.createFunction = async (ctx, functionSpecification) => { | ||
let functionCreated = false; | ||
try { | ||
let url = `${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ | ||
ctx.body.subscriptionId | ||
}/boundary/${ctx.body.boundaryId}/function/${ctx.body.functionId}`; | ||
// Acquire any additional configuration elements from optional components | ||
let additionalCfg = {}; | ||
// Is storage requested? | ||
if (functionSpecification.enableStorage) { | ||
delete functionSpecification.enableStorage; | ||
additionalCfg = exports.createStorage(additionalCfg); | ||
if (typeof functionSpecification.nodejs.files['package.json'] === 'object') { | ||
functionSpecification.nodejs.files['package.json'].dependencies['@fusebit/add-on-sdk'] = '*'; | ||
} else if (typeof functionSpecification.nodejs.files['package.json'] === 'string') { | ||
let pkg = JSON.parse(functionSpecification.nodejs.files['package.json']); | ||
pkg.dependencies['@fusebit/add-on-sdk'] = '*'; | ||
functionSpecification.nodejs.files['package.json'] = pkg; | ||
} | ||
} | ||
// Add the additional configuration elements to the specification | ||
if (functionSpecification.configurationSerialized) { | ||
functionSpecification.configurationSerialized += `# Storage configuration settings | ||
${Object.keys(additionalCfg) | ||
.sort() | ||
.map((k) => `${k}=${ctx.body.configuration[k]}`) | ||
.join('\n')} | ||
`; | ||
} else { | ||
functionSpecification.configuration = { ...functionSpecification.configuration, ...additionalCfg }; | ||
} | ||
// Create the function | ||
let url = `${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ctx.body.subscriptionId}/boundary/${ctx.body.boundaryId}/function/${ctx.body.functionId}`; | ||
let response = await Superagent.put(url) | ||
.set("Authorization", ctx.headers['authorization']) // pass-through authorization | ||
.set('Authorization', ctx.headers['authorization']) // pass-through authorization | ||
.send(functionSpecification); | ||
functionCreated = true; | ||
// Wait for the function to be built and ready | ||
let attempts = 15; | ||
while (response.status === 201 && attempts > 0) { | ||
response = await Superagent.get( | ||
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ | ||
ctx.body.subscriptionId | ||
}/boundary/${ctx.body.boundaryId}/function/${ctx.body.functionId}/build/${response.body.buildId}` | ||
).set("Authorization", ctx.headers['authorization']); | ||
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ctx.body.subscriptionId}/boundary/${ctx.body.boundaryId}/function/${ctx.body.functionId}/build/${response.body.buildId}` | ||
).set('Authorization', ctx.headers['authorization']); | ||
if (response.status === 200) { | ||
if (response.body.status === "success") { | ||
if (response.body.status === 'success') { | ||
return; | ||
} else { | ||
throw new Error( | ||
`Failure creating function: ${(response.body.error && | ||
response.body.error.message) || | ||
"Unknown error"}` | ||
`Failure creating function: ${(response.body.error && response.body.error.message) || 'Unknown error'}` | ||
); | ||
} | ||
} | ||
await new Promise(resolve => setTimeout(resolve, 2000)); | ||
await new Promise((resolve) => setTimeout(resolve, 2000)); | ||
attempts--; | ||
@@ -245,3 +348,4 @@ } | ||
} | ||
if (response.status === 204 || (response.body && response.body.status === "success")) { | ||
if (response.status === 204 || (response.body && response.body.status === 'success')) { | ||
return; | ||
@@ -251,4 +355,3 @@ } else { | ||
} | ||
} | ||
catch (e) { | ||
} catch (e) { | ||
if (functionCreated) { | ||
@@ -265,6 +368,32 @@ try { | ||
await Superagent.delete( | ||
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ | ||
ctx.body.subscriptionId | ||
}/boundary/${boundaryId || ctx.body.boundaryId}/function/${functionId || ctx.body.functionId}` | ||
).set("Authorization", ctx.headers['authorization']); // pass-through authorization | ||
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ctx.body.subscriptionId}/boundary/${ | ||
boundaryId || ctx.body.boundaryId | ||
}/function/${functionId || ctx.body.functionId}` | ||
).set('Authorization', ctx.headers['authorization']); // pass-through authorization | ||
}; | ||
exports.getStorageClient = (ctx) => { | ||
const accessToken = Jwt.sign({}, Buffer.from(ctx.configuration.fusebit_storage_key, 'base64').toString('utf8'), { | ||
algorithm: 'RS256', | ||
expiresIn: 60 * 60 * 24, | ||
audience: ctx.configuration.fusebit_storage_audience, | ||
issuer: ctx.configuration.fusebit_storage_issuer_id, | ||
subject: ctx.configuration.fusebit_storage_subject, | ||
keyid: ctx.configuration.fusebit_storage_key_id, | ||
header: { jwtId: Date.now().toString() }, | ||
}); | ||
const url = `${ctx.configuration.fusebit_storage_audience}/v1/account/${ctx.configuration.fusebit_storage_account_id}/subscription/${ctx.configuration.fusebit_storage_subscription_id}/storage/${ctx.configuration.fusebit_storage_id}`; | ||
return { | ||
get: async () => { | ||
const response = await Superagent.get(url) | ||
.set('Authorization', `Bearer ${accessToken}`) | ||
.ok((res) => res.status < 300 || res.status === 404); | ||
return response.status === 404 ? undefined : response.body.data; | ||
}, | ||
put: async (data) => { | ||
await Superagent.put(url).set('Authorization', `Bearer ${accessToken}`).send({ data }); | ||
}, | ||
}; | ||
}; |
{ | ||
"name": "@fusebit/add-on-sdk", | ||
"version": "0.0.1", | ||
"version": "1.0.2", | ||
"description": "SDK for implementing Fusebit Add-Ons", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
18452
5
362
1
1