@fusebit/add-on-sdk
Advanced tools
Comparing version 2.1.0 to 3.0.0
254
lib/index.js
const Superagent = require('superagent'); | ||
const Url = require('url'); | ||
const Jwt = require('jsonwebtoken'); | ||
const uuid = require('uuid'); | ||
@@ -175,6 +173,3 @@ function debug() { | ||
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}`; | ||
return ctx.baseUrl; | ||
}; | ||
@@ -192,148 +187,9 @@ | ||
function generateIssuerSubject(ctx) { | ||
return { | ||
issuerId: `uri:fusebit-template:${ctx.functionId}:${ctx.body.subscriptionId}:${ctx.body.boundaryId}:${ctx.body.functionId}`, | ||
subject: 'client-1', | ||
}; | ||
} | ||
exports.createStorage = async (ctx) => { | ||
let issuerCreated = false; | ||
let { issuerId, subject } = generateIssuerSubject(ctx); | ||
let clientId; | ||
try { | ||
// Create a PKI issuer to represent the the Add-on Handler | ||
debug(`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 })) | ||
) | ||
); | ||
debug('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 ${ctx.functionId} add-on handler ${ctx.body.subscriptionId}/${ctx.body.boundaryId}/${ctx.body.functionId}`, | ||
publicKeys: [{ keyId, publicKey }], | ||
}); | ||
issuerCreated = true; | ||
debug('ISSUER CREATED'); | ||
// Create a Client for the add-on handler with permissions to storage | ||
const storageId = uuid.v4(); | ||
debug(`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 ${ctx.functionId} 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; | ||
debug('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; | ||
} | ||
}; | ||
function generateStorageUrl(storageCtx) { | ||
return `${storageCtx.fusebit_storage_audience}/v1/account/${storageCtx.fusebit_storage_account_id}/subscription/${storageCtx.fusebit_storage_subscription_id}/storage/${storageCtx.fusebit_storage_id}`; | ||
} | ||
exports.deleteStorage = async (ctx, storageCtx) => { | ||
debug('DELETE STORAGE'); | ||
const issuerId = storageCtx.fusebit_storage_issuer_id; | ||
const subject = storageCtx.fusebit_storage_subject; | ||
const accountId = storageCtx.fusebit_storage_account_id; | ||
if (!issuerId || !subject || !accountId) { | ||
debug('Storage not configured for this function'); | ||
return; | ||
} | ||
// Delete the storage | ||
debug('Deleting storage'); | ||
await Superagent.delete(generateStorageUrl(storageCtx)) | ||
.set('Authorization', ctx.headers['authorization']) // pass-through authorization | ||
.ok((r) => r.status < 300 || r.status === 404); | ||
debug('Deleted storage'); | ||
// Find the client | ||
debug('Looking up Client ID', issuerId); | ||
const response = await Superagent.get( | ||
`${ctx.body.baseUrl}/v1/account/${accountId}/client?issuerId=${encodeURIComponent(issuerId)}&subject=${subject}&include=all` | ||
).set('Authorization', ctx.headers['authorization']); // pass-through authorization | ||
const client = response.body.items && response.body.items[0]; | ||
debug('Found client', client); | ||
if (client) { | ||
// Delete the client | ||
debug('Deleting client'); | ||
await Superagent.delete(`${ctx.body.baseUrl}/v1/account/${accountId}/client/${client.id}`) | ||
.set('Authorization', ctx.headers['authorization']) // pass-through authorization | ||
.ok((r) => r.status < 300 || r.status === 404); | ||
debug('Deleted client'); | ||
} | ||
// Delete the issuer | ||
debug('Deleting issuer', issuerId); | ||
await Superagent.delete(`${ctx.body.baseUrl}/v1/account/${accountId}/issuer/${encodeURIComponent(issuerId)}`) | ||
.set('Authorization', ctx.headers['authorization']) // pass-through authorization | ||
.ok((r) => r.status < 300 || r.status === 404); | ||
debug('Deleted issuer'); | ||
}; | ||
exports.createFunction = async (ctx, functionSpecification) => { | ||
exports.createFunction = async (ctx, functionSpecification, accessToken) => { | ||
let functionCreated = false; | ||
const accessTokenHeader = `Bearer ${accessToken}`; | ||
try { | ||
// 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 | ||
.send(functionSpecification); | ||
let response = await Superagent.put(url).set('Authorization', accessTokenHeader).send(functionSpecification); | ||
functionCreated = true; | ||
@@ -346,6 +202,6 @@ | ||
`${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']); | ||
).set('Authorization', accessTokenHeader); | ||
if (response.status === 200) { | ||
if (response.body.status === 'success') { | ||
return; | ||
break; | ||
} else { | ||
@@ -365,10 +221,18 @@ throw new Error( | ||
if (response.status === 204 || (response.body && response.body.status === 'success')) { | ||
return; | ||
} else { | ||
throw response.body; | ||
if (response.body && response.body.location) { | ||
return response.body.location; | ||
} else { | ||
response = await Superagent.get( | ||
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ctx.body.subscriptionId}/boundary/${ctx.body.boundaryId}/function/${ctx.body.functionId}/location` | ||
).set('Authorization', accessTokenHeader); | ||
if (response.body && response.body.location) { | ||
return response.body.location; | ||
} | ||
} | ||
} | ||
throw response.body; | ||
} catch (e) { | ||
if (functionCreated) { | ||
try { | ||
await exports.deleteFunction(ctx); | ||
await exports.deleteFunction(ctx, accessToken); | ||
} catch (_) {} | ||
@@ -380,3 +244,3 @@ } | ||
exports.deleteFunction = async (ctx, boundaryId, functionId) => { | ||
exports.deleteFunction = async (ctx, accessToken, boundaryId, functionId) => { | ||
await Superagent.delete( | ||
@@ -386,11 +250,13 @@ `${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ctx.body.subscriptionId}/boundary/${ | ||
}/function/${functionId || ctx.body.functionId}` | ||
).set('Authorization', ctx.headers['authorization']); // pass-through authorization | ||
) | ||
.set('Authorization', `Bearer ${accessToken}`) | ||
.ok((res) => res.status === 204 || res.status === 404); | ||
}; | ||
exports.getFunctionDefinition = async (ctx, boundaryId, functionId) => { | ||
let response = await Superagent.get( | ||
exports.getFunctionDefinition = async (ctx, accessToken, boundaryId, functionId) => { | ||
const response = await Superagent.get( | ||
`${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 | ||
).set('Authorization', `Bearer ${accessToken}`); | ||
@@ -400,8 +266,8 @@ return response.body; | ||
exports.getFunctionUrl = async (ctx, boundaryId, functionId) => { | ||
let response = await Superagent.get( | ||
exports.getFunctionUrl = async (ctx, accessToken, boundaryId, functionId) => { | ||
const response = await Superagent.get( | ||
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${ctx.body.subscriptionId}/boundary/${ | ||
boundaryId || ctx.body.boundaryId | ||
}/function/${functionId || ctx.body.functionId}/location` | ||
).set('Authorization', ctx.headers['authorization']); // pass-through authorization | ||
).set('Authorization', `Bearer ${accessToken}`); | ||
@@ -411,36 +277,48 @@ return response.body.location; | ||
exports.getStorageClient = (ctx) => { | ||
const expiry = 60 * 16; // 15+1 min to align with Lambda lifecycle plus some buffer | ||
const accessToken = Jwt.sign({}, Buffer.from(ctx.configuration.fusebit_storage_key, 'base64').toString('utf8'), { | ||
algorithm: 'RS256', | ||
expiresIn: expiry, | ||
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 removeLeadingSlash = (s) => s.replace(/^\/(.+)$/, '$1'); | ||
const removeTrailingSlash = (s) => s.replace(/^(.+)\/$/, '$1'); | ||
const url = generateStorageUrl(ctx.configuration); | ||
exports.createStorageClient = async (ctx, accessToken, storageIdPrefix) => { | ||
storageIdPrefix = storageIdPrefix ? removeLeadingSlash(removeTrailingSlash(storageIdPrefix)) : ''; | ||
const functionUrl = Url.parse(ctx.baseUrl); | ||
let storageBaseUrl = `${functionUrl.protocol}//${functionUrl.host}/v1/account/${ctx.accountId}/subscription/${ | ||
ctx.subscriptionId | ||
}/storage${storageIdPrefix ? '/' + storageIdPrefix : ''}`; | ||
return { | ||
etag: null, | ||
expiration: Date.now() + expiry * 1000, | ||
get: async function () { | ||
const response = await Superagent.get(url) | ||
const getUrl = (storageSubId) => { | ||
storageSubId = storageSubId ? removeTrailingSlash(removeLeadingSlash(storageSubId)) : ''; | ||
return `${storageBaseUrl}${storageSubId ? '/' + storageSubId : ''}`; | ||
}; | ||
const storageClient = { | ||
get: async function (storageSubId) { | ||
const response = await Superagent.get(getUrl(storageSubId)) | ||
.set('Authorization', `Bearer ${accessToken}`) | ||
.ok((res) => res.status < 300 || res.status === 404); | ||
this.etag = response.body.etag; | ||
return response.status === 404 ? undefined : response.body.data; | ||
return response.status === 404 ? undefined : response.body; | ||
}, | ||
put: async function (data, force) { | ||
let request = Superagent.put(url).set('Authorization', `Bearer ${accessToken}`); | ||
let payload = { data: data }; | ||
if (!force) { | ||
payload.etag = this.etag; | ||
put: async function (data, storageSubId) { | ||
const response = await Superagent.put(getUrl(storageSubId)).set('Authorization', `Bearer ${accessToken}`).send(data); | ||
return response.body; | ||
}, | ||
delete: async function (storageSubId, recursive, forceRecursive) { | ||
storageSubId = storageSubId ? removeLeadingSlash(removeTrailingSlash(storageSubId)) : ''; | ||
if (!storageSubId && !storageIdPrefix && recursive && !forceRecursive) { | ||
throw new Error( | ||
'You are attempting to recursively delete all storage objects in the Fusebit subscription. If this is your intent, please pass "true" as the third parameter in the call to delete(storageSubId, recursive, forceRecursive).' | ||
); | ||
} | ||
const response = await request.send(payload); | ||
this.etag = response.body.etag; | ||
await Superagent.delete(`${getUrl(storageSubId)}${recursive ? '/*' : ''}`).set('Authorization', `Bearer ${accessToken}`); | ||
return; | ||
}, | ||
list: async function (storageSubId, { count, next }) { | ||
const response = await Superagent.get(`${getUrl(storageSubId)}/*`) | ||
.query(isNaN(count) ? undefined : { count }) | ||
.query(typeof next === 'string' ? { next } : undefined) | ||
.set('Authorization', `Bearer ${accessToken}`); | ||
return response.body; | ||
}, | ||
}; | ||
return storageClient; | ||
}; |
{ | ||
"name": "@fusebit/add-on-sdk", | ||
"version": "2.1.0", | ||
"version": "3.0.0", | ||
"description": "SDK for implementing Fusebit Add-Ons", | ||
@@ -11,5 +11,3 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"jsonwebtoken": "^8.5.1", | ||
"superagent": "^5.2.2", | ||
"uuid": "^8.2.0" | ||
"superagent": "^6.1.0" | ||
}, | ||
@@ -16,0 +14,0 @@ "devDependencies": { |
1
15744
298
+ Addedsuperagent@6.1.0(transitive)
- Removedjsonwebtoken@^8.5.1
- Removeduuid@^8.2.0
- Removedbuffer-equal-constant-time@1.0.1(transitive)
- Removedecdsa-sig-formatter@1.0.11(transitive)
- Removedjsonwebtoken@8.5.1(transitive)
- Removedjwa@1.4.1(transitive)
- Removedjws@3.2.2(transitive)
- Removedlodash.includes@4.3.0(transitive)
- Removedlodash.isboolean@3.0.3(transitive)
- Removedlodash.isinteger@4.0.4(transitive)
- Removedlodash.isnumber@3.0.3(transitive)
- Removedlodash.isplainobject@4.0.6(transitive)
- Removedlodash.isstring@4.0.1(transitive)
- Removedlodash.once@4.1.1(transitive)
- Removedsemver@5.7.2(transitive)
- Removedsuperagent@5.3.1(transitive)
- Removeduuid@8.3.2(transitive)
Updatedsuperagent@^6.1.0