@walkeros/server-destination-aws 4.7.0-next-1790722899482 → 4.7.0
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.
- package/CHANGELOG.md +21 -3
- package/README.md +16 -3
- package/dist/dev.d.mts +76 -50
- package/dist/dev.d.ts +76 -50
- package/dist/dev.js +1 -1
- package/dist/dev.js.map +1 -1
- package/dist/dev.mjs +1 -1
- package/dist/dev.mjs.map +1 -1
- package/dist/examples/index.d.mts +47 -35
- package/dist/examples/index.d.ts +47 -35
- package/dist/examples/index.js +181 -102
- package/dist/examples/index.mjs +179 -100
- package/dist/index.d.mts +161 -60
- package/dist/index.d.ts +161 -60
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/schemas.js +1 -1
- package/dist/schemas.js.map +1 -1
- package/dist/schemas.mjs +1 -1
- package/dist/schemas.mjs.map +1 -1
- package/dist/walkerOS.json +1331 -129
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.create;var mod,__defProp=Object.defineProperty,__getOwnPropDesc=Object.getOwnPropertyDescriptor,__getOwnPropNames=Object.getOwnPropertyNames,__hasOwnProp=(Object.getPrototypeOf,Object.prototype.hasOwnProperty),__copyProps=(to,from,except,desc)=>{if(from&&"object"==typeof from||"function"==typeof from)for(let key of __getOwnPropNames(from))__hasOwnProp.call(to,key)||key===except||__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to},index_exports={};((target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})})(index_exports,{DestinationFirehose:()=>types_exports,DestinationSNS:()=>types_exports2,default:()=>destinationFirehose,destinationFirehose:()=>destinationFirehose,destinationSNS:()=>destinationSNS}),module.exports=(mod=index_exports,__copyProps(__defProp({},"__esModule",{value:!0}),mod));var import_core2=require("@walkeros/core"),import_core=require("@walkeros/core");function isAWSEnvironment(env){return Boolean(env&&"object"==typeof env&&"AWS"in env&&env.AWS?.FirehoseClient)}function getConfig(partialConfig={},env){const settings=partialConfig.settings||{};return settings.firehose&&(settings.firehose=function(firehoseConfig,env){const{streamName:streamName,region:region="eu-central-1",config:config={}}=firehoseConfig;streamName||(0,import_core.throwError)("Firehose: Config custom streamName missing"),config.region||(config.region=region);let client=firehoseConfig.client;return!client&&isAWSEnvironment(env)&&(client=new env.AWS.FirehoseClient(config)),{streamName:streamName,client:client,region:region}}(settings.firehose,env)),{...partialConfig,settings:settings}}var push=async function(event,context){const{firehose:firehose}=context.config.settings||{};firehose&&async function(pushEvents,config,context){const{client:client,streamName:streamName}=config,{env:env,logger:logger}=context;if(!client)return{queue:pushEvents};const records=pushEvents.map(({event:event})=>({Data:Buffer.from(JSON.stringify(event))}));if(logger.debug("Calling AWS Firehose API",{stream:streamName,recordCount:records.length}),isAWSEnvironment(env))await client.send(new env.AWS.PutRecordBatchCommand({DeliveryStreamName:streamName,Records:records}));else{const{PutRecordBatchCommand:PutRecordBatchCommand}=await import("@aws-sdk/client-firehose");await client.send(new PutRecordBatchCommand({DeliveryStreamName:streamName,Records:records}))}logger?.debug("AWS Firehose API response",{ok:!0})}([{event:event}],firehose,context)},types_exports={},destinationFirehose={type:"aws-firehose",config:{},async init({config:partialConfig,env:env,logger:logger}){const config=getConfig(partialConfig,env);return(0,import_core2.isSameType)(config.settings,{})||logger.throw("Config settings invalid"),config},push:async(event,context)=>await push(event,context)},import_core3=require("@walkeros/core");function isAWSEnvironment2(env){if("object"!=typeof env||null===env)return!1;if(!("AWS"in env))return!1;const aws=env.AWS;return"object"==typeof aws&&null!==aws&&"SNSClient"in aws}function getConfig2(partial={},env){const settings=function(partial,env){const{topicName:topicName,topicArn:topicArn,config:config={}}=partial,region=partial.region??"eu-central-1";topicName||(0,import_core3.throwError)("SNS: Settings topicName missing");const sdkConfig=config.region?config:{...config,region:region};let client=partial.client;!client&&isAWSEnvironment2(env)&&(client=new env.AWS.SNSClient(sdkConfig));const resolved={topicName:topicName,region:region,config:sdkConfig};return client&&(resolved.client=client),topicArn&&(resolved.topicArn=topicArn),resolved}(partial.settings&&"object"==typeof partial.settings?partial.settings:{},env);return{...partial,settings:settings}}var import_core4=require("@walkeros/core");function isMessageAttribute(value){if(!(0,import_core4.isObject)(value))return!1;return"string"==typeof value.DataType}async function resolveStringMappingValue(event,value,collector){const resolved=await(0,import_core4.getMappingValue)(event,value,{collector:collector});return"string"==typeof resolved&&resolved.length>0?resolved:null!=resolved?String(resolved):void 0}var push2=async function(event,context){const{config:config,env:env,rule:rule,collector:collector,logger:logger,id:id}=context,settings=config.settings,client=settings?.client,topicArn=settings?.topicArn;if(!client)return logger.throw("SNS push: client missing, init() not run");if(!topicArn)return logger.throw("SNS push: topicArn missing, init() not run");if(!isAWSEnvironment2(env))return logger.throw("SNS push: env.AWS not injected");const ruleSettings=(value=rule?.settings,"object"==typeof value&&null!==value?rule.settings:{});var value;let messageGroupId,messageDeduplicationId;void 0!==ruleSettings.messageGroupId&&(messageGroupId=await resolveStringMappingValue(event,ruleSettings.messageGroupId,collector)),void 0!==ruleSettings.messageDeduplicationId&&(messageDeduplicationId=await resolveStringMappingValue(event,ruleSettings.messageDeduplicationId,collector));const messageAttributes=await async function(event,source,collector){if(!source)return;const result={};let touched=!1;for(const[key,value]of Object.entries(source)){const resolved=await(0,import_core4.getMappingValue)(event,value,{collector:collector});isMessageAttribute(resolved)?(result[key]=resolved,touched=!0):"string"==typeof resolved&&resolved.length>0?(result[key]={DataType:"String",StringValue:resolved},touched=!0):null!=resolved&&(result[key]={DataType:"String",StringValue:String(resolved)},touched=!0)}return touched?result:void 0}(event,ruleSettings.messageAttributes,collector),input={TopicArn:topicArn,Message:JSON.stringify(event)};messageGroupId&&(input.MessageGroupId=messageGroupId),messageDeduplicationId&&(input.MessageDeduplicationId=messageDeduplicationId),messageAttributes&&(input.MessageAttributes=messageAttributes),logger.debug("SNS Publish",{topicArn:topicArn,fifo:topicArn.endsWith(".fifo"),event:event.name});try{await client.send(new env.AWS.PublishCommand(input))}catch(err){throw logger.error(`SNS Publish failed for "${topicArn}". Run "walkeros setup destination.${id}" if the topic is missing.`,{topicArn:topicArn,event:event.name,error:err instanceof Error?err.message:String(err)}),err}},import_core5=require("@walkeros/core"),DEFAULT_SETUP={region:"eu-central-1",fifoTopic:!1},accountIdCache=new Map;function buildSubscribeAttributes(sub){const attrs={};return void 0!==sub.rawMessageDelivery&&(attrs.RawMessageDelivery=String(sub.rawMessageDelivery)),void 0!==sub.filterPolicy&&(attrs.FilterPolicy=JSON.stringify(sub.filterPolicy)),void 0!==sub.deadLetterTargetArn&&(attrs.RedrivePolicy=JSON.stringify({deadLetterTargetArn:sub.deadLetterTargetArn})),Object.keys(attrs).length>0?attrs:void 0}var types_exports2={};var destinationSNS={type:"aws-sns",config:{},setup:async function(ctx){const{config:config,env:env,logger:logger}=ctx,merged=(0,import_core5.resolveSetup)(config.setup,DEFAULT_SETUP);if(!merged)return void logger.debug("setup: skipped (config.setup is false or unset)");const settings=config.settings;if(!settings||!settings.topicName)return void logger.throw("setup: settings.topicName is required. There is no safe default for the SNS topic name.");const region=merged.region??DEFAULT_SETUP.region,fifo=merged.fifoTopic??!1,declaredName=settings.topicName,finalName=function(name,fifo){if(fifo)return name.endsWith(".fifo")?name:`${name}.fifo`;if(name.endsWith(".fifo"))throw new Error(`FIFO suffix '.fifo' on standard topic name '${name}'. Set setup.fifoTopic: true or rename the topic.`);return name}(declaredName,fifo);fifo&&finalName!==declaredName&&logger.info(`setup: appended .fifo suffix to FIFO topic name '${declaredName}' -> '${finalName}'`);const client=function(settings,env,region){if(settings?.client)return settings.client;const config=settings?.config??{},merged=config.region?config:{...config,region:region};return new env.AWS.SNSClient(merged)}(settings,env,region),accountId=await async function(env,region){const cached=accountIdCache.get(region);if(cached)return cached;const stsClient=new env.AWS.STSClient({region:region}),res=await stsClient.send(new env.AWS.GetCallerIdentityCommand({}));if("object"==typeof res&&null!==res&&"Account"in res&&"string"==typeof res.Account){const accountId=res.Account;return accountIdCache.set(region,accountId),accountId}throw new Error("STS GetCallerIdentity returned no Account")}(env,region),candidateArn=`arn:aws:sns:${region}:${accountId}:${finalName}`;let topicCreated=!1;try{await client.send(new env.AWS.GetTopicAttributesCommand({TopicArn:candidateArn}))}catch(err){if(!function(err){return!(!function(err){return"object"==typeof err&&null!==err}(err)||"NotFoundException"!==err.name&&"NotFound"!==err.name&&(!err.$metadata||404!==err.$metadata.httpStatusCode))}(err))throw err;topicCreated=!0}const attributes={};fifo&&(attributes.FifoTopic="true",attributes.ContentBasedDeduplication="true"),void 0!==merged.displayName&&(attributes.DisplayName=merged.displayName),void 0!==merged.kmsMasterKeyId&&(attributes.KmsMasterKeyId=merged.kmsMasterKeyId);const tagEntries=merged.tags?Object.entries(merged.tags).map(([Key,Value])=>({Key:Key,Value:Value})):void 0,createInput={Name:finalName};Object.keys(attributes).length>0&&(createInput.Attributes=attributes),tagEntries&&tagEntries.length>0&&(createInput.Tags=tagEntries);const topicArn=function(res){if("object"==typeof(v=res)&&null!==v&&"string"==typeof res.TopicArn)return res.TopicArn;var v;throw new Error("SNS CreateTopic returned no TopicArn")}(await client.send(new env.AWS.CreateTopicCommand(createInput)));topicCreated?logger.info("setup: topic created",{topicArn:topicArn,region:region}):logger.debug("setup: topic exists; declared state re-applied",{topicArn:topicArn,region:region});const subscriptionsCreated=await async function(client,env,topicArn,declared){let count=0;for(const sub of declared){const input={TopicArn:topicArn,Protocol:sub.protocol,Endpoint:sub.endpoint},attrs=buildSubscribeAttributes(sub);attrs&&(input.Attributes=attrs),await client.send(new env.AWS.SubscribeCommand(input)),count+=1}return count}(client,env,topicArn,merged.subscriptions??[]);return{topicArn:topicArn,topicCreated:topicCreated,tagsApplied:merged.tags?Object.keys(merged.tags).length:0,subscriptionsCreated:subscriptionsCreated}},async init({config:partial,env:env,logger:logger,id:id}){const config=getConfig2(partial,env);if(!config.settings.client)return void logger.throw("SNS init: client missing (env not injected)");if(!isAWSEnvironment2(env))return void logger.throw("SNS init: env.AWS missing");if(config.settings.topicArn)return config;const{client:client,topicName:topicName,region:region}=config.settings;try{const res=await client.send(new env.AWS.CreateTopicCommand({Name:topicName}));if("object"!=typeof(v=res)||null===v||"string"!=typeof res.TopicArn||0===res.TopicArn.length)return void logger.throw(`SNS init: CreateTopic returned no ARN for "${topicName}"`);config.settings.topicArn=res.TopicArn}catch(err){throw logger.error(`SNS init failed for topic "${topicName}" in region ${region??"eu-central-1"}. Either set settings.topicArn directly, run "walkeros setup destination.${id}", or grant sns:CreateTopic to the runtime role.`,{topicName:topicName,region:region,error:err instanceof Error?err.message:String(err)}),err}var v;return config},push:async(event,context)=>await push2(event,context)};//# sourceMappingURL=index.js.map
|
|
1
|
+
"use strict";var mod,__defProp=Object.defineProperty,__getOwnPropDesc=Object.getOwnPropertyDescriptor,__getOwnPropNames=Object.getOwnPropertyNames,__hasOwnProp=Object.prototype.hasOwnProperty,index_exports={};((target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})})(index_exports,{DestinationFirehose:()=>types_exports,DestinationSNS:()=>types_exports2,default:()=>destinationFirehose,destinationFirehose:()=>destinationFirehose,destinationSNS:()=>destinationSNS}),module.exports=(mod=index_exports,((to,from,except,desc)=>{if(from&&"object"==typeof from||"function"==typeof from)for(let key of __getOwnPropNames(from))__hasOwnProp.call(to,key)||key===except||__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to})(__defProp({},"__esModule",{value:!0}),mod));var import_client_firehose=require("@aws-sdk/client-firehose"),import_core2=require("@walkeros/core"),import_config=require("@smithy/core/config");function isHandlerInstance(handler){return"object"==typeof handler&&null!==handler&&"handle"in handler&&"function"==typeof handler.handle}function resolveTimeout(timeout){return void 0===timeout||!Number.isFinite(timeout)||timeout<=0?1e4:timeout}function requestHandlerOptions(timeout){return{requestTimeout:Math.floor(.25*resolveTimeout(timeout)),throwOnRequestTimeout:!0}}function parseTopicArn(arn){const parts=arn.split(":");if(6!==parts.length)return;const[prefix,partition,service,region,accountId,name]=parts;return"arn"===prefix&&partition&&"sns"===service&®ion&&accountId&&name?{region:region,accountId:accountId,name:name}:void 0}async function loadSdkRegion(){try{const region=await(0,import_config.loadConfig)(import_config.NODE_REGION_CONFIG_OPTIONS,import_config.NODE_REGION_CONFIG_FILE_OPTIONS)();return"string"==typeof region&®ion.length>0?region:void 0}catch{return}}function firstString(...candidates){return candidates.find(candidate=>"string"==typeof candidate&&candidate.length>0)}var InFlight=class{pending=new Set;track(promise){this.pending.add(promise);const remove=()=>{this.pending.delete(promise)};return promise.then(remove,remove),promise}get size(){return this.pending.size}async settle(){for(;this.pending.size>0;)await Promise.allSettled([...this.pending])}},import_core=require("@walkeros/core"),import_retry=require("@smithy/core/retry"),AwsDeliveryError=class extends Error{code;status;retryable;constructor(message,options){super(message,{cause:options.cause}),this.name="AwsDeliveryError",this.code=options.code,this.status=options.status,this.retryable=options.retryable}};function formatMessage(target,code,text){const hint=function(code,target){switch(code){case"ResourceNotFoundException":return"Firehose"===target.service?"Check settings.streamName and settings.region.":"Check settings.topicArn and settings.region.";case"AccessDeniedException":case"AuthorizationErrorException":return"Firehose"===target.service?"Allow firehose:PutRecordBatch on the stream for these credentials.":"Allow sns:Publish on the topic for these credentials.";case"UnrecognizedClientException":case"InvalidSignatureException":case"ExpiredTokenException":case"CredentialsProviderError":return"Check config.credentials or the AWS credentials in the environment.";case"NotFoundException":return"SNS"===target.service?`Run "walkeros setup destination.${target.id??"<id>"}" or set settings.topicArn.`:void 0;default:return}}(code,target),base=`${function(target){const kind="Firehose"===target.service?"stream":"topic";return`${target.service} ${kind} "${target.resource}" (${target.region})`}(target)}: ${code}: ${text}`;return hint?`${base} ${hint}`:base}function fromSdkError(error,target){if(error instanceof AwsDeliveryError)return error;if(!function(error){return error instanceof Error}(error))return new AwsDeliveryError(formatMessage(target,"UnknownError",String(error)),{code:"UnknownError",retryable:!1,cause:error});const code=function(error){if("Error"!==error.name)return error.name;const code=Reflect.get(error,"code");return"string"==typeof code&&code.length>0?code:error.name}(error),retryable=(0,import_retry.isThrottlingError)(error)||(0,import_retry.isTransientError)(error)||(0,import_retry.isServerError)(error);return new AwsDeliveryError(formatMessage(target,code,error.message),{code:code,status:error.$metadata?.httpStatusCode,retryable:retryable,cause:error})}function recordError(target,code,text,status){return new AwsDeliveryError(formatMessage(target,code,text),{code:code,status:status,retryable:!0})}function localError(target,code,text,cause){return new AwsDeliveryError(formatMessage(target,code,text),{code:code,retryable:"InvalidResponse"===code,cause:cause})}function configError(message){return new AwsDeliveryError(message,{code:"InvalidConfig",retryable:!1})}function nonEmpty(value){return"string"==typeof value&&value.length>0}function parseCredentials(input){if(null==input||""===input)return;let value=input;if("string"==typeof input)try{value=JSON.parse(input)}catch{throw configError('config.credentials is not valid JSON. Expected {"accessKeyId", "secretAccessKey", "sessionToken"?}.')}if(!(0,import_core.isObject)(value))throw configError("config.credentials must be an object with accessKeyId and secretAccessKey.");const{accessKeyId:accessKeyId,secretAccessKey:secretAccessKey,sessionToken:sessionToken}=value;for(const[field,fieldValue]of[["accessKeyId",accessKeyId],["secretAccessKey",secretAccessKey]])if(!nonEmpty(fieldValue))throw configError(`config.credentials.${field} is missing or empty. Set it from a $secret.`);if(void 0!==sessionToken&&!nonEmpty(sessionToken))throw configError("config.credentials.sessionToken must be a non-empty string when set.");const credentials={accessKeyId:String(accessKeyId),secretAccessKey:String(secretAccessKey)};return void 0!==sessionToken&&(credentials.sessionToken=sessionToken),credentials}var STREAM_NAME_PATTERN=/^[a-zA-Z0-9_.-]{1,64}$/;function sameValue(a,b){if(a===b)return!0;if(Array.isArray(a)&&Array.isArray(b))return a.length===b.length&&a.every((v,i)=>sameValue(v,b[i]));if(!(0,import_core2.isObject)(a)||!(0,import_core2.isObject)(b))return!1;if(!isPlain(a)||!isPlain(b))return!1;const keys=Object.keys(a);return keys.length===Object.keys(b).length&&keys.every(key=>key in b&&sameValue(a[key],b[key]))}function isPlain(value){const proto=Object.getPrototypeOf(value);return proto===Object.prototype||null===proto}function pick(key,flat,alias){if(void 0!==flat&&void 0!==alias&&!sameValue(flat,alias))throw configError(`Firehose: settings.${key} and settings.firehose.${key} are both set and differ. Remove settings.firehose.`);return flat??alias}async function getConfig(partialConfig={},env,logger){const input=partialConfig.settings??{},alias=input.firehose??{},streamName=pick("streamName",input.streamName,alias.streamName),sdkConfig=pick("config",input.config,alias.config),userClient=pick("client",input.client,alias.client),regionSetting=pick("region",input.region,alias.region);if(!streamName)throw configError("Firehose: settings.streamName is missing. Set it to the name of your Firehose stream.");if(!STREAM_NAME_PATTERN.test(streamName))throw configError(`Firehose: settings.streamName "${streamName}" is not a valid stream name. Use 1 to 64 letters, digits, "_", "." or "-".`);const region=firstString(regionSetting,"string"==typeof sdkConfig?.region?sdkConfig.region:void 0,await loadSdkRegion())??"eu-central-1",credentials=parseCredentials(partialConfig.credentials),credentialSource=userClient?"client":credentials?"config.credentials":void 0!==sdkConfig?.credentials?"settings.config.credentials":"default chain",client=userClient??function(Client,options){const clientConfig2={...options.config,region:options.region};return options.credentials&&(clientConfig2.credentials=options.credentials),void 0===clientConfig2.requestHandler&&(clientConfig2.requestHandler=requestHandlerOptions(options.timeout)),new Client(clientConfig2)}(env?.AWS?.FirehoseClient??import_client_firehose.FirehoseClient,{region:region,config:sdkConfig,credentials:credentials,timeout:partialConfig.timeout}),settings={streamName:streamName,region:region,newline:!1!==input.newline,client:client,runtime:{ownsClient:!userClient&&!isHandlerInstance(sdkConfig?.requestHandler),inflight:new InFlight,credentialSource:credentialSource}};return sdkConfig&&(settings.config=sdkConfig),logger.debug("Firehose client ready",{stream:streamName,region:region,credentials:credentialSource}),{...partialConfig,settings:settings}}function resolveSettings(settings){const{streamName:streamName,region:region,client:client,runtime:runtime}=settings??{};if(!(streamName&®ion&&client&&runtime))throw configError("Firehose: the destination was not initialized.");return{streamName:streamName,region:region,newline:!1!==settings?.newline,client:client,runtime:runtime}}var import_client_firehose2=require("@aws-sdk/client-firehose"),import_core3=require("@walkeros/core");function toPayload(event,data){return(0,import_core3.isObject)(data)?data:event}function planChunks(payloads,newline){const chunks=[],refused=[];let current=[],currentBytes=0;return payloads.forEach((payload,index)=>{let data;try{data=function(payload,newline=!0){const json=JSON.stringify(payload);if("string"!=typeof json)throw new TypeError("The payload does not serialize to JSON.");return Buffer.from(newline?`${json}\n`:json)}(payload,newline)}catch(error){return void refused.push({index:index,code:"InvalidRecord",detail:`Record ${index} does not serialize to JSON: ${error instanceof Error?error.message:String(error)}`,cause:error})}data.byteLength>1e6?refused.push({index:index,code:"RecordTooLarge",detail:`Record ${index} is ${data.byteLength} bytes, over the 1000000 byte limit.`}):((current.length>=500||currentBytes+data.byteLength>29e5)&&(chunks.push(current),current=[],currentBytes=0),current.push({index:index,data:data}),currentBytes+=data.byteLength)}),current.length>0&&chunks.push(current),{chunks:chunks,refused:refused}}function isRecord(value){return"object"==typeof value&&null!==value}function readMetadata(value){if(!isRecord(value))return{};const metadata=value.$metadata;if(!isRecord(metadata))return{};const{httpStatusCode:httpStatusCode,attempts:attempts}=metadata;return{status:"number"==typeof httpStatusCode?httpStatusCode:void 0,attempts:"number"==typeof attempts?attempts:void 0}}async function deliver(items,settings,env,logger){const target={service:"Firehose",resource:settings.streamName,region:settings.region},Command=env?.AWS?.PutRecordBatchCommand??import_client_firehose2.PutRecordBatchCommand,plan=planChunks(items.map(({event:event,data:data})=>toPayload(event,data)),settings.newline),failures=plan.refused.map(({index:index,code:code,detail:detail,cause:cause})=>({index:index,error:localError(target,code,detail,cause)})),results=await Promise.all(plan.chunks.map(async chunk=>{const started=Date.now(),bytes=chunk.reduce((sum,record)=>sum+record.data.byteLength,0),command=new Command({DeliveryStreamName:settings.streamName,Records:chunk.map(({data:data})=>({Data:data}))});let result,attempts;try{const response=function(value){const response=readMetadata(value);return isRecord(value)?("number"==typeof value.FailedPutCount&&(response.failedPutCount=value.FailedPutCount),Array.isArray(value.RequestResponses)&&(response.responses=value.RequestResponses.map(entry=>{if(!isRecord(entry))return{};const{ErrorCode:ErrorCode,ErrorMessage:ErrorMessage}=entry;return{errorCode:"string"==typeof ErrorCode&&ErrorCode.length>0?ErrorCode:void 0,errorMessage:"string"==typeof ErrorMessage?ErrorMessage:void 0}})),response):response}(await settings.runtime.inflight.track(settings.client.send(command)));attempts=response.attempts,result=function(chunk,response,target){const{responses:responses,failedPutCount:failedPutCount,status:status}=response;if(responses&&responses.length===chunk.length){const failures=[];if(responses.forEach((entry,local)=>{entry.errorCode&&failures.push({index:chunk[local].index,error:recordError(target,entry.errorCode,entry.errorMessage??"Record rejected.",status)})}),void 0===failedPutCount||failures.length===failedPutCount)return failures}else if(0===failedPutCount)return[];const error=localError(target,"InvalidResponse",`PutRecordBatch answered without per-record results that match its FailedPutCount for ${chunk.length} records (FailedPutCount ${failedPutCount??"missing"}).`);return chunk.map(({index:index})=>({index:index,error:error}))}(chunk,response,target)}catch(thrown){attempts=readMetadata(thrown).attempts;const error=fromSdkError(thrown,target);result=chunk.map(({index:index})=>({index:index,error:error}))}return logger.debug("Firehose PutRecordBatch",{stream:settings.streamName,records:chunk.length,bytes:bytes,failed:result.length,attempts:attempts,ms:Date.now()-started}),result}));for(const result of results)failures.push(...result);return failures.sort((a,b)=>a.index-b.index)}function toOutcome(total,failures){if(0!==failures.length){if(failures.length>=total)throw function(failures){const first=failures[0].error;if(failures.every(({error:error})=>error===first))return first;const byCode=new Map;for(const{error:error}of failures){const entry=byCode.get(error.code);entry?entry.count+=1:byCode.set(error.code,{count:1,sample:error})}let dominant={count:0,sample:first};for(const entry of byCode.values())entry.count>dominant.count&&(dominant=entry);const counts=[...byCode.entries()].map(([code,{count:count}])=>`${count} ${code}`).join(", ");return new AwsDeliveryError(`${dominant.sample.message} (all ${failures.length} records failed: ${counts})`,{code:dominant.sample.code,status:dominant.sample.status,retryable:failures.every(({error:error})=>error.retryable),cause:dominant.sample})}(failures);return{failed:failures.map(({index:index,error:error})=>({index:index,error:error}))}}}var types_exports={},destinationFirehose={type:"aws-firehose",config:{batch:{size:500,age:1e3}},init:async({config:config,env:env,logger:logger})=>getConfig(config,env,logger),push:async function(event,context){const{config:config,data:data,env:env,logger:logger}=context,settings=resolveSettings(config.settings),[failure]=await deliver([{event:event,data:data}],settings,env,logger);if(failure)throw failure.error},pushBatch:async(batch,context)=>{const{config:config,env:env,logger:logger}=context;if(0===batch.entries.length)return;const settings=resolveSettings(config.settings),failures=await deliver(batch.entries.map(({event:event,data:data})=>({event:event,data:data})),settings,env,logger);return toOutcome(batch.entries.length,failures)},async destroy({config:config,logger:logger}){const runtime=config.settings?.runtime;runtime&&(await runtime.inflight.settle(),runtime.ownsClient&&(runtime.ownsClient=!1,config.settings?.client?.destroy?.(),logger.debug("Firehose client closed")))}},import_client_sns=require("@aws-sdk/client-sns"),import_client_sts=require("@aws-sdk/client-sts"),import_core4=require("@walkeros/core");function createSnsClient(env,options){return new(env?.AWS?.SNSClient??import_client_sns.SNSClient)(function(options){const resolved={...options.config,region:options.region};return options.credentials&&(resolved.credentials=options.credentials),void 0===resolved.requestHandler&&(resolved.requestHandler=requestHandlerOptions(options.timeout)),resolved}(options))}function createStsClient(env,options){const Client=env?.AWS?.STSClient??import_client_sts.STSClient,config={region:options.region},credentials=options.credentials??options.config?.credentials;credentials&&(config.credentials=credentials);const userHandler=options.config?.requestHandler;return config.requestHandler=void 0===userHandler||isHandlerInstance(userHandler)?requestHandlerOptions(options.timeout):userHandler,config.maxAttempts=1,new Client(config)}async function getCallerIdentity(client,env,target){const Command=env?.AWS?.GetCallerIdentityCommand??import_client_sts.GetCallerIdentityCommand;let response;try{response=await client.send(new Command({}))}catch(error){throw fromSdkError(error,target)}if(!(0,import_core4.isObject)(response)||"string"!=typeof response.Account)throw fromSdkError(new Error("STS GetCallerIdentity returned no Account."),target);const partition=("string"==typeof response.Arn?response.Arn:"").split(":")[1]||"aws";return{accountId:response.Account,partition:partition}}function topicArnResolver(topicArn,topicName,target,stsClient,env){if(topicArn)return async()=>topicArn;let pending;return()=>(pending??=(async()=>{const client=stsClient();try{const{accountId:accountId,partition:partition}=await getCallerIdentity(client,env,target);return`arn:${partition}:sns:${target.region}:${accountId}:${topicName}`}finally{client.destroy?.()}})().catch(error=>{throw pending=void 0,error}),pending)}var import_client_sns2=require("@aws-sdk/client-sns"),import_core5=require("@walkeros/core");function isMessageAttribute(value){return(0,import_core5.isObject)(value)&&"string"==typeof value.DataType}async function resolveStringMappingValue(event,value,collector){const resolved=await(0,import_core5.getMappingValue)(event,value,{collector:collector});return"string"==typeof resolved&&resolved.length>0?resolved:null!=resolved?String(resolved):void 0}function readAttempts(value){if("object"!=typeof value||null===value||!("$metadata"in value))return;const metadata=value.$metadata;return"object"==typeof metadata&&null!==metadata&&"attempts"in metadata&&"number"==typeof metadata.attempts?metadata.attempts:void 0}var import_client_sns3=require("@aws-sdk/client-sns"),import_core6=require("@walkeros/core"),DEFAULT_SETUP={fifoTopic:!1},identityCache=new Map;function buildSubscribeAttributes(sub){const attrs={};return void 0!==sub.rawMessageDelivery&&(attrs.RawMessageDelivery=String(sub.rawMessageDelivery)),void 0!==sub.filterPolicy&&(attrs.FilterPolicy=JSON.stringify(sub.filterPolicy)),void 0!==sub.deadLetterTargetArn&&(attrs.RedrivePolicy=JSON.stringify({deadLetterTargetArn:sub.deadLetterTargetArn})),Object.keys(attrs).length>0?attrs:void 0}var types_exports2={},destinationSNS={type:"aws-sns",config:{},setup:async function(ctx){const{config:config,env:env,logger:logger}=ctx,merged=(0,import_core6.resolveSetup)(config.setup,DEFAULT_SETUP);if(!merged)return void logger.debug("setup: skipped (config.setup is false or unset)");const settings=config.settings??{},arn=settings.topicArn?parseTopicArn(settings.topicArn):void 0,topicName=settings.topicName??arn?.name;if(!topicName)return void logger.throw("setup: settings.topicName (or settings.topicArn) is required. There is no safe default for the SNS topic name.");const sdkRegion="string"==typeof settings.config?.region?settings.config.region:void 0,region=firstString(merged.region,settings.region,arn?.region,sdkRegion,await loadSdkRegion())??"eu-central-1",fifo=merged.fifoTopic??!1,finalName=function(name,fifo){if(fifo)return name.endsWith(".fifo")?name:`${name}.fifo`;if(name.endsWith(".fifo"))throw new Error(`FIFO suffix '.fifo' on standard topic name '${name}'. Set setup.fifoTopic: true or rename the topic.`);return name}(topicName,fifo);fifo&&finalName!==topicName&&logger.info(`setup: appended .fifo suffix to FIFO topic name '${topicName}' -> '${finalName}'`);const options={region:region,config:settings.config,credentials:parseCredentials(config.credentials),timeout:config.timeout},client=settings.client??createSnsClient(env,options),closesClient=!settings.client&&!isHandlerInstance(settings.config?.requestHandler);try{const{accountId:accountId,partition:partition}=await async function(env,options,target){const cached=identityCache.get(options.region);if(cached)return cached;const stsClient=createStsClient(env,options);try{const identity=await getCallerIdentity(stsClient,env,target);return identityCache.set(options.region,identity),identity}finally{stsClient.destroy?.()}}(env,options,{service:"SNS",resource:finalName,region:region,id:ctx.id}),candidateArn=`arn:${partition}:sns:${region}:${accountId}:${finalName}`;let topicCreated=!1;try{const Command=env?.AWS?.GetTopicAttributesCommand??import_client_sns3.GetTopicAttributesCommand;await client.send(new Command({TopicArn:candidateArn}))}catch(err){if(!function(err){return!(!function(err){return"object"==typeof err&&null!==err}(err)||"NotFoundException"!==err.name&&"NotFound"!==err.name&&(!err.$metadata||404!==err.$metadata.httpStatusCode))}(err))throw err;topicCreated=!0}const attributes={};fifo&&(attributes.FifoTopic="true",attributes.ContentBasedDeduplication="true"),void 0!==merged.displayName&&(attributes.DisplayName=merged.displayName),void 0!==merged.kmsMasterKeyId&&(attributes.KmsMasterKeyId=merged.kmsMasterKeyId);const tagEntries=merged.tags?Object.entries(merged.tags).map(([Key,Value])=>({Key:Key,Value:Value})):void 0,createInput={Name:finalName};Object.keys(attributes).length>0&&(createInput.Attributes=attributes),tagEntries&&tagEntries.length>0&&(createInput.Tags=tagEntries);const CreateCommand=env?.AWS?.CreateTopicCommand??import_client_sns3.CreateTopicCommand,topicArn=function(res){if("object"==typeof(v=res)&&null!==v&&"string"==typeof res.TopicArn)return res.TopicArn;var v;throw new Error("SNS CreateTopic returned no TopicArn")}(await client.send(new CreateCommand(createInput)));topicCreated?logger.info("setup: topic created",{topicArn:topicArn,region:region}):logger.debug("setup: topic exists; declared state re-applied",{topicArn:topicArn,region:region});const subscriptionsCreated=await async function(client,env,topicArn,declared){let count=0;for(const sub of declared){const input={TopicArn:topicArn,Protocol:sub.protocol,Endpoint:sub.endpoint},attrs=buildSubscribeAttributes(sub);attrs&&(input.Attributes=attrs);const Command=env?.AWS?.SubscribeCommand??import_client_sns3.SubscribeCommand;await client.send(new Command(input)),count+=1}return count}(client,env,topicArn,merged.subscriptions??[]);return{topicArn:topicArn,topicCreated:topicCreated,tagsApplied:merged.tags?Object.keys(merged.tags).length:0,subscriptionsCreated:subscriptionsCreated}}finally{closesClient&&client.destroy?.()}},init:async({config:config,env:env,logger:logger,id:id})=>async function(partialConfig={},env,logger,id){const input=partialConfig.settings??{},{topicArn:topicArn,config:sdkConfig,client:userClient}=input,arn=topicArn?parseTopicArn(topicArn):void 0;if(topicArn&&!arn)throw configError(`SNS: settings.topicArn "${topicArn}" is not an SNS topic ARN (arn:aws:sns:<region>:<account>:<name>).`);if(!arn&&!input.topicName)throw configError("SNS: settings.topicArn is missing. Set it to the ARN of your topic (or set settings.topicName).");if(arn&&input.region&&input.region!==arn.region)throw configError(`SNS: settings.region "${input.region}" differs from the region "${arn.region}" in settings.topicArn. Remove settings.region.`);if(arn&&input.topicName&&input.topicName!==arn.name)throw configError(`SNS: settings.topicName "${input.topicName}" differs from the name "${arn.name}" in settings.topicArn. Remove settings.topicName.`);const sdkRegion="string"==typeof sdkConfig?.region?sdkConfig.region:void 0,region=firstString(input.region,arn?.region,sdkRegion,await loadSdkRegion())??"eu-central-1",topicName=input.topicName??arn?.name;void 0!==partialConfig.batch&&logger.warn("SNS publishes one message per event and does not batch. Remove config.batch from this destination.");const credentials=parseCredentials(partialConfig.credentials),clientOptions={region:region,config:sdkConfig,credentials:credentials,timeout:partialConfig.timeout},settings={region:region,client:userClient??createSnsClient(env,clientOptions),runtime:{ownsClient:!userClient&&!isHandlerInstance(sdkConfig?.requestHandler),inflight:new InFlight,topicArn:topicArnResolver(topicArn,topicName,{service:"SNS",resource:topicName??"",region:region,id:id},()=>createStsClient(env,clientOptions),env)}};return topicArn&&(settings.topicArn=topicArn),topicName&&(settings.topicName=topicName),sdkConfig&&(settings.config=sdkConfig),logger.debug("SNS client ready",{topic:topicArn??topicName,region:region,credentials:userClient?"client":credentials?"config.credentials":void 0!==sdkConfig?.credentials?"settings.config.credentials":"default chain"}),{...partialConfig,settings:settings}}(config,env,logger,id),push:async function(event,context){const{config:config,data:data,env:env,rule:rule,collector:collector,logger:logger,id:id}=context,settings=function(settings){const{region:region,client:client,runtime:runtime}=settings??{};if(!region||!client||!runtime)throw configError("SNS: the destination was not initialized.");return{topicArn:settings?.topicArn,topicName:settings?.topicName,region:region,client:client,runtime:runtime}}(config.settings),{runtime:runtime}=settings,topicArn=await runtime.inflight.track(runtime.topicArn()),target={service:"SNS",resource:topicArn,region:settings.region,id:id},ruleSettings=(value=rule?.settings,"object"==typeof value&&null!==value?rule.settings:{});var value;const messageGroupId=void 0!==ruleSettings.messageGroupId?await resolveStringMappingValue(event,ruleSettings.messageGroupId,collector):void 0,fifo=topicArn.endsWith(".fifo"),messageDeduplicationId=(void 0!==ruleSettings.messageDeduplicationId?await resolveStringMappingValue(event,ruleSettings.messageDeduplicationId,collector):void 0)??(fifo&&event.id?event.id:void 0),messageAttributes=await async function(event,source,collector){if(!source)return;const result={};let touched=!1;for(const[key,value]of Object.entries(source)){const resolved=await(0,import_core5.getMappingValue)(event,value,{collector:collector});isMessageAttribute(resolved)?(result[key]=resolved,touched=!0):"string"==typeof resolved&&resolved.length>0?(result[key]={DataType:"String",StringValue:resolved},touched=!0):null!=resolved&&(result[key]={DataType:"String",StringValue:String(resolved)},touched=!0)}return touched?result:void 0}(event,ruleSettings.messageAttributes,collector);let message;try{message=JSON.stringify(toPayload(event,data))}catch(error){throw localError(target,"InvalidRecord",`The message does not serialize to JSON: ${error instanceof Error?error.message:String(error)}`,error)}const input={TopicArn:topicArn,Message:message};messageGroupId&&(input.MessageGroupId=messageGroupId),messageDeduplicationId&&(input.MessageDeduplicationId=messageDeduplicationId),messageAttributes&&(input.MessageAttributes=messageAttributes);const Command=env?.AWS?.PublishCommand??import_client_sns2.PublishCommand,started=Date.now();let attempts;try{attempts=readAttempts(await runtime.inflight.track(settings.client.send(new Command(input))))}catch(error){throw attempts=readAttempts(error),fromSdkError(error,target)}finally{logger.debug("SNS Publish",{topicArn:topicArn,fifo:fifo,event:event.name,attempts:attempts,ms:Date.now()-started})}},async destroy({config:config,logger:logger}){const runtime=config.settings?.runtime;runtime&&(await runtime.inflight.settle(),runtime.ownsClient&&(runtime.ownsClient=!1,config.settings?.client?.destroy?.(),logger.debug("SNS client closed")))}};//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/firehose/index.ts","../src/firehose/lib/firehose.ts","../src/firehose/config.ts","../src/firehose/push.ts","../src/firehose/types/index.ts","../src/sns/lib/sns.ts","../src/sns/config.ts","../src/sns/push.ts","../src/sns/setup.ts","../src/sns/types/index.ts","../src/sns/index.ts"],"sourcesContent":["// AWS Firehose\nexport { destinationFirehose } from './firehose';\nexport * as DestinationFirehose from './firehose/types';\n\n// AWS SNS\nexport { destinationSNS } from './sns';\nexport * as DestinationSNS from './sns/types';\n\n// Default remains Firehose for backward compatibility.\nexport { destinationFirehose as default } from './firehose';\n","import type { Settings, Destination } from './types';\nimport { isSameType } from '@walkeros/core';\nimport { getConfig } from './config';\nimport { push } from './push';\n\n// Types\nexport * as DestinationFirehose from './types';\n\nexport const destinationFirehose: Destination = {\n type: 'aws-firehose',\n\n config: {},\n\n async init({ config: partialConfig, env, logger }) {\n const config = getConfig(partialConfig, env);\n\n if (!isSameType(config.settings, {} as Settings))\n logger.throw('Config settings invalid');\n\n return config;\n },\n\n async push(event, context) {\n return await push(event, context);\n },\n};\n\nexport default destinationFirehose;\n","import type { Destination, Logger } from '@walkeros/core';\nimport type { FirehoseConfig, Env } from '../types';\nimport { throwError } from '@walkeros/core';\n\n// Type guard to check if environment has AWS SDK\nfunction isAWSEnvironment(env: unknown): env is Env {\n return Boolean(\n env &&\n typeof env === 'object' &&\n 'AWS' in env &&\n (env as Env).AWS?.FirehoseClient,\n );\n}\n\nexport function getConfigFirehose(\n firehoseConfig: Partial<FirehoseConfig>,\n env?: unknown,\n): FirehoseConfig {\n const { streamName, region = 'eu-central-1', config = {} } = firehoseConfig;\n\n if (!streamName) throwError('Firehose: Config custom streamName missing');\n\n if (!config.region) config.region = region;\n\n // Use environment-injected SDK or fall back to provided client\n let client = firehoseConfig.client;\n if (!client && isAWSEnvironment(env)) {\n client = new env.AWS.FirehoseClient(config);\n }\n\n return {\n streamName,\n client,\n region,\n };\n}\n\nexport async function pushFirehose(\n pushEvents: Destination.PushEvents,\n config: FirehoseConfig,\n context: Destination.PushContext,\n) {\n const { client, streamName } = config;\n const { env, logger } = context;\n\n if (!client) return { queue: pushEvents };\n\n // Up to 500 records per batch\n const records = pushEvents.map(({ event }) => ({\n Data: Buffer.from(JSON.stringify(event)),\n }));\n\n logger.debug('Calling AWS Firehose API', {\n stream: streamName,\n recordCount: records.length,\n });\n\n // Use environment-injected SDK command or fall back to direct import\n if (isAWSEnvironment(env)) {\n await client.send(\n new env.AWS.PutRecordBatchCommand({\n DeliveryStreamName: streamName,\n Records: records,\n }),\n );\n } else {\n // Fall back to direct import for backward compatibility\n const { PutRecordBatchCommand } = await import('@aws-sdk/client-firehose');\n await client.send(\n new PutRecordBatchCommand({\n DeliveryStreamName: streamName,\n Records: records,\n }),\n );\n }\n\n logger?.debug('AWS Firehose API response', { ok: true });\n}\n","import type { Config, Settings, PartialConfig, Env } from './types';\nimport { getConfigFirehose } from './lib/firehose';\n\nexport function getConfig(\n partialConfig: PartialConfig = {},\n env?: unknown,\n): Config {\n const settings = partialConfig.settings || ({} as Settings);\n\n if (settings.firehose)\n settings.firehose = getConfigFirehose(settings.firehose, env);\n\n return { ...partialConfig, settings };\n}\n","import type { PushFn } from './types';\nimport { pushFirehose } from './lib/firehose';\n\nexport const push: PushFn = async function (event, context) {\n const { firehose } = context.config.settings || {};\n\n if (firehose) pushFirehose([{ event }], firehose, context);\n\n return;\n};\n","import type { DestinationServer } from '@walkeros/server-core';\nimport type {\n Mapping as WalkerOSMapping,\n Destination as CoreDestination,\n} from '@walkeros/core';\nimport type {\n FirehoseClientConfig,\n PutRecordBatchCommandInput,\n} from '@aws-sdk/client-firehose';\n\n/**\n * The part of an AWS SDK v3 client this destination calls: `send` with a\n * command. `FirehoseClient` satisfies it, and so does an injected mock (tests,\n * simulate) without a cast.\n */\nexport interface SendClient {\n send(command: object): Promise<unknown>;\n}\n\nexport interface Settings {\n firehose?: FirehoseConfig;\n}\n\nexport type InitSettings = Partial<Settings>;\n\nexport interface Mapping {}\n\nexport interface Env extends DestinationServer.Env {\n AWS: {\n FirehoseClient: new (config: FirehoseClientConfig) => SendClient;\n PutRecordBatchCommand: new (input: PutRecordBatchCommandInput) => {\n readonly input: PutRecordBatchCommandInput;\n };\n };\n}\n\nexport type Types = CoreDestination.Types<Settings, Mapping, Env, InitSettings>;\n\nexport interface Destination extends DestinationServer.Destination<Types> {\n init: DestinationServer.InitFn<Types>;\n}\n\nexport type Config = {\n settings: Settings;\n} & DestinationServer.Config<Types>;\n\nexport type InitFn = DestinationServer.InitFn<Types>;\nexport type PushFn = DestinationServer.PushFn<Types>;\n\nexport type PartialConfig = DestinationServer.PartialConfig<Types>;\n\nexport type PushEvents = DestinationServer.PushEvents<Mapping>;\n\nexport type Rule = WalkerOSMapping.Rule<Mapping>;\nexport type Rules = WalkerOSMapping.Rules<Rule>;\n\nexport interface FirehoseConfig {\n streamName: string;\n client?: SendClient;\n region?: string;\n config?: FirehoseClientConfig;\n}\n","import type { Env, Settings } from '../types';\nimport { throwError } from '@walkeros/core';\n\n/** Type guard to check if environment has the AWS SNS SDK injected. */\nexport function isAWSEnvironment(env: unknown): env is Env {\n if (typeof env !== 'object' || env === null) return false;\n if (!('AWS' in env)) return false;\n const aws = (env as { AWS?: unknown }).AWS;\n if (typeof aws !== 'object' || aws === null) return false;\n return 'SNSClient' in aws;\n}\n\n/**\n * Resolve `Settings` from a partial input. Validates `topicName`, fills\n * `region` from default, builds an SNSClient from the env-injected\n * constructor when none was supplied.\n */\nexport function getConfigSNS(\n partial: Partial<Settings>,\n env?: unknown,\n): Settings {\n const { topicName, topicArn, config = {} } = partial;\n const region = partial.region ?? 'eu-central-1';\n\n if (!topicName) throwError('SNS: Settings topicName missing');\n\n const sdkConfig = config.region ? config : { ...config, region };\n\n let client = partial.client;\n if (!client && isAWSEnvironment(env)) {\n client = new env.AWS.SNSClient(sdkConfig);\n }\n\n const resolved: Settings = {\n topicName,\n region,\n config: sdkConfig,\n };\n if (client) resolved.client = client;\n if (topicArn) resolved.topicArn = topicArn;\n return resolved;\n}\n","import type { Config, PartialConfig, Settings } from './types';\nimport { getConfigSNS } from './lib/sns';\n\nexport function getConfig(partial: PartialConfig = {}, env?: unknown): Config {\n const partialSettings: Partial<Settings> =\n partial.settings && typeof partial.settings === 'object'\n ? partial.settings\n : {};\n const settings = getConfigSNS(partialSettings, env);\n return { ...partial, settings };\n}\n","import type { PushFn, Mapping } from './types';\nimport type { Mapping as WalkerOSMapping, Collector } from '@walkeros/core';\nimport { getMappingValue, isObject } from '@walkeros/core';\nimport { isAWSEnvironment } from './lib/sns';\n\ninterface MessageAttribute {\n DataType: string;\n StringValue?: string;\n BinaryValue?: Uint8Array;\n}\n\nfunction isMessageAttribute(value: unknown): value is MessageAttribute {\n if (!isObject(value)) return false;\n const candidate = value as Record<string, unknown>;\n return typeof candidate.DataType === 'string';\n}\n\nasync function resolveMessageAttributes(\n event: Parameters<PushFn>[0],\n source: WalkerOSMapping.Map | undefined,\n collector: Collector.Instance,\n): Promise<Record<string, MessageAttribute> | undefined> {\n if (!source) return undefined;\n const result: Record<string, MessageAttribute> = {};\n let touched = false;\n for (const [key, value] of Object.entries(source)) {\n const resolved = await getMappingValue(event, value, { collector });\n if (isMessageAttribute(resolved)) {\n result[key] = resolved;\n touched = true;\n } else if (typeof resolved === 'string' && resolved.length > 0) {\n // Bare-string convenience: wrap into String DataType.\n result[key] = { DataType: 'String', StringValue: resolved };\n touched = true;\n } else if (resolved !== undefined && resolved !== null) {\n result[key] = { DataType: 'String', StringValue: String(resolved) };\n touched = true;\n }\n }\n return touched ? result : undefined;\n}\n\nfunction isMapping(value: unknown): value is Mapping {\n return typeof value === 'object' && value !== null;\n}\n\nasync function resolveStringMappingValue(\n event: Parameters<PushFn>[0],\n value: WalkerOSMapping.Value,\n collector: Collector.Instance,\n): Promise<string | undefined> {\n const resolved = await getMappingValue(event, value, { collector });\n if (typeof resolved === 'string' && resolved.length > 0) return resolved;\n if (resolved === undefined || resolved === null) return undefined;\n return String(resolved);\n}\n\nexport const push: PushFn = async function (event, context) {\n const { config, env, rule, collector, logger, id } = context;\n const settings = config.settings;\n const client = settings?.client;\n const topicArn = settings?.topicArn;\n\n if (!client) return logger.throw('SNS push: client missing, init() not run');\n if (!topicArn)\n return logger.throw('SNS push: topicArn missing, init() not run');\n if (!isAWSEnvironment(env))\n return logger.throw('SNS push: env.AWS not injected');\n\n const ruleSettings: Mapping = isMapping(rule?.settings) ? rule.settings : {};\n\n let messageGroupId: string | undefined;\n if (ruleSettings.messageGroupId !== undefined) {\n messageGroupId = await resolveStringMappingValue(\n event,\n ruleSettings.messageGroupId,\n collector,\n );\n }\n\n let messageDeduplicationId: string | undefined;\n if (ruleSettings.messageDeduplicationId !== undefined) {\n messageDeduplicationId = await resolveStringMappingValue(\n event,\n ruleSettings.messageDeduplicationId,\n collector,\n );\n }\n\n const messageAttributes = await resolveMessageAttributes(\n event,\n ruleSettings.messageAttributes,\n collector,\n );\n\n const input: {\n TopicArn: string;\n Message: string;\n MessageGroupId?: string;\n MessageDeduplicationId?: string;\n MessageAttributes?: Record<string, MessageAttribute>;\n } = {\n TopicArn: topicArn,\n Message: JSON.stringify(event),\n };\n if (messageGroupId) input.MessageGroupId = messageGroupId;\n if (messageDeduplicationId)\n input.MessageDeduplicationId = messageDeduplicationId;\n if (messageAttributes) input.MessageAttributes = messageAttributes;\n\n logger.debug('SNS Publish', {\n topicArn,\n fifo: topicArn.endsWith('.fifo'),\n event: event.name,\n });\n\n try {\n await client.send(new env.AWS.PublishCommand(input));\n } catch (err) {\n logger.error(\n `SNS Publish failed for \"${topicArn}\". ` +\n `Run \"walkeros setup destination.${id}\" if the topic is missing.`,\n {\n topicArn,\n event: event.name,\n error: err instanceof Error ? err.message : String(err),\n },\n );\n throw err;\n }\n};\n","import type { DestinationServer } from '@walkeros/server-core';\nimport type {\n Env,\n SendClient,\n Setup,\n SetupSubscription,\n Settings,\n Types,\n} from './types';\nimport type { LifecycleContext } from '@walkeros/core';\nimport { resolveSetup } from '@walkeros/core';\n\n// Setup is wired to the destination's `setup` slot which uses the broader\n// `DestinationServer.Config<Types>` (settings is optional). We runtime-narrow\n// instead of using the local Config alias so the assignment in index.ts\n// type-checks without contravariance issues.\ntype WideConfig = DestinationServer.Config<Types>;\n\nexport const DEFAULT_SETUP: Required<Pick<Setup, 'region' | 'fifoTopic'>> &\n Setup = {\n region: 'eu-central-1',\n fifoTopic: false,\n};\n\nexport interface SetupResult {\n topicArn: string;\n /** True when the GetTopicAttributes probe returned 404 (topic absent before this run). */\n topicCreated: boolean;\n tagsApplied: number;\n subscriptionsCreated: number;\n}\n\n// Module-level account-ID cache so re-running setup in the same process is\n// a no-op for the STS GetCallerIdentity call. Keyed by region (in case\n// future versions support cross-region setup in one process).\nconst accountIdCache: Map<string, string> = new Map();\n\n/** Test-only: clear the account-ID cache between test cases. */\nexport function __resetAccountIdCache(): void {\n accountIdCache.clear();\n}\n\ninterface CreateTopicResponse {\n TopicArn?: string;\n}\n\ninterface AwsErrorMeta {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n}\n\nfunction isAwsError(err: unknown): err is AwsErrorMeta {\n return typeof err === 'object' && err !== null;\n}\n\nfunction isNotFound(err: unknown): boolean {\n if (!isAwsError(err)) return false;\n if (err.name === 'NotFoundException' || err.name === 'NotFound') return true;\n if (err.$metadata && err.$metadata.httpStatusCode === 404) return true;\n return false;\n}\n\nfunction resolveTopicName(name: string, fifo: boolean): string {\n if (fifo) {\n if (name.endsWith('.fifo')) return name;\n return `${name}.fifo`;\n }\n if (name.endsWith('.fifo')) {\n throw new Error(\n `FIFO suffix '.fifo' on standard topic name '${name}'. Set setup.fifoTopic: true or rename the topic.`,\n );\n }\n return name;\n}\n\nfunction resolveClient(\n settings: Settings | undefined,\n env: Env,\n region: string,\n): SendClient {\n if (settings?.client) return settings.client;\n const config = settings?.config ?? {};\n const merged = config.region ? config : { ...config, region };\n return new env.AWS.SNSClient(merged);\n}\n\nasync function getAccountId(env: Env, region: string): Promise<string> {\n const cached = accountIdCache.get(region);\n if (cached) return cached;\n const stsClient = new env.AWS.STSClient({ region });\n const res: unknown = await stsClient.send(\n new env.AWS.GetCallerIdentityCommand({}),\n );\n if (\n typeof res === 'object' &&\n res !== null &&\n 'Account' in res &&\n typeof (res as { Account?: unknown }).Account === 'string'\n ) {\n const accountId = (res as { Account: string }).Account;\n accountIdCache.set(region, accountId);\n return accountId;\n }\n throw new Error('STS GetCallerIdentity returned no Account');\n}\n\nfunction isCreateTopicResponse(v: unknown): v is CreateTopicResponse {\n return typeof v === 'object' && v !== null;\n}\n\nfunction extractTopicArn(res: unknown): string {\n if (isCreateTopicResponse(res) && typeof res.TopicArn === 'string')\n return res.TopicArn;\n throw new Error('SNS CreateTopic returned no TopicArn');\n}\n\nfunction buildSubscribeAttributes(\n sub: SetupSubscription,\n): Record<string, string> | undefined {\n const attrs: Record<string, string> = {};\n if (sub.rawMessageDelivery !== undefined) {\n attrs.RawMessageDelivery = String(sub.rawMessageDelivery);\n }\n if (sub.filterPolicy !== undefined) {\n attrs.FilterPolicy = JSON.stringify(sub.filterPolicy);\n }\n if (sub.deadLetterTargetArn !== undefined) {\n attrs.RedrivePolicy = JSON.stringify({\n deadLetterTargetArn: sub.deadLetterTargetArn,\n });\n }\n return Object.keys(attrs).length > 0 ? attrs : undefined;\n}\n\nasync function applyDeclaredSubscriptions(\n client: SendClient,\n env: Env,\n topicArn: string,\n declared: SetupSubscription[],\n): Promise<number> {\n let count = 0;\n for (const sub of declared) {\n const input: {\n TopicArn: string;\n Protocol: string;\n Endpoint: string;\n Attributes?: Record<string, string>;\n } = {\n TopicArn: topicArn,\n Protocol: sub.protocol,\n Endpoint: sub.endpoint,\n };\n const attrs = buildSubscribeAttributes(sub);\n if (attrs) input.Attributes = attrs;\n await client.send(new env.AWS.SubscribeCommand(input));\n count += 1;\n }\n return count;\n}\n\nexport async function setup(\n ctx: LifecycleContext<WideConfig, Env>,\n): Promise<SetupResult | undefined> {\n const { config, env, logger } = ctx;\n const merged = resolveSetup(config.setup, DEFAULT_SETUP);\n if (!merged) {\n logger.debug('setup: skipped (config.setup is false or unset)');\n return;\n }\n\n const settings = config.settings;\n if (!settings || !settings.topicName) {\n logger.throw(\n 'setup: settings.topicName is required. There is no safe default for the SNS topic name.',\n );\n return;\n }\n\n const region = merged.region ?? DEFAULT_SETUP.region;\n const fifo = merged.fifoTopic ?? false;\n const declaredName = settings.topicName;\n const finalName = resolveTopicName(declaredName, fifo);\n if (fifo && finalName !== declaredName) {\n logger.info(\n `setup: appended .fifo suffix to FIFO topic name '${declaredName}' -> '${finalName}'`,\n );\n }\n\n // Topic-existence probe via candidate ARN derived from STS account ID.\n const client = resolveClient(settings, env, region);\n const accountId = await getAccountId(env, region);\n const candidateArn = `arn:aws:sns:${region}:${accountId}:${finalName}`;\n\n let topicCreated = false;\n try {\n await client.send(\n new env.AWS.GetTopicAttributesCommand({ TopicArn: candidateArn }),\n );\n } catch (err) {\n if (isNotFound(err)) {\n topicCreated = true;\n } else {\n throw err;\n }\n }\n\n // Authoritative-apply: full declared state in one CreateTopic call.\n // SNS:CreateTopic is idempotent on identical Name+Attributes+Tags inputs.\n const attributes: Record<string, string> = {};\n if (fifo) {\n attributes.FifoTopic = 'true';\n attributes.ContentBasedDeduplication = 'true';\n }\n if (merged.displayName !== undefined)\n attributes.DisplayName = merged.displayName;\n if (merged.kmsMasterKeyId !== undefined)\n attributes.KmsMasterKeyId = merged.kmsMasterKeyId;\n\n const tagEntries = merged.tags\n ? Object.entries(merged.tags).map(([Key, Value]) => ({ Key, Value }))\n : undefined;\n\n const createInput: {\n Name: string;\n Attributes?: Record<string, string>;\n Tags?: Array<{ Key: string; Value: string }>;\n } = { Name: finalName };\n if (Object.keys(attributes).length > 0) createInput.Attributes = attributes;\n if (tagEntries && tagEntries.length > 0) createInput.Tags = tagEntries;\n\n const createRes = await client.send(\n new env.AWS.CreateTopicCommand(createInput),\n );\n const topicArn = extractTopicArn(createRes);\n\n if (topicCreated) {\n logger.info('setup: topic created', { topicArn, region });\n } else {\n logger.debug('setup: topic exists; declared state re-applied', {\n topicArn,\n region,\n });\n }\n\n const subscriptionsCreated = await applyDeclaredSubscriptions(\n client,\n env,\n topicArn,\n merged.subscriptions ?? [],\n );\n\n return {\n topicArn,\n topicCreated,\n tagsApplied: merged.tags ? Object.keys(merged.tags).length : 0,\n subscriptionsCreated,\n };\n}\n","import type { DestinationServer } from '@walkeros/server-core';\nimport type {\n Mapping as WalkerOSMapping,\n Destination as CoreDestination,\n SetupFn as CoreSetupFn,\n} from '@walkeros/core';\nimport type {\n SNSClientConfig,\n CreateTopicCommandInput,\n PublishCommandInput,\n GetTopicAttributesCommandInput,\n SubscribeCommandInput,\n} from '@aws-sdk/client-sns';\nimport type {\n STSClientConfig,\n GetCallerIdentityCommandInput,\n} from '@aws-sdk/client-sts';\n\n/**\n * The part of an AWS SDK v3 client this destination calls: `send` with a\n * command. `SNSClient` and `STSClient` satisfy it, and so does an injected\n * mock (tests, simulate) without a cast.\n */\nexport interface SendClient {\n send(command: object): Promise<unknown>;\n}\n\n/** An AWS SDK v3 command class, as far as this destination builds one. */\nexport type CommandConstructor<Input> = new (input: Input) => {\n readonly input: Input;\n};\n\nexport interface Settings {\n /** Topic name (without `.fifo` suffix unless fifoTopic is true). REQUIRED. */\n topicName: string;\n /** Pre-configured client. Optional; created from env if absent. */\n client?: SendClient;\n /** AWS region. Mirrors setup.region default ('eu-central-1'). */\n region?: string;\n /** SDK client config (credentials, etc.). Optional. */\n config?: SNSClientConfig;\n /** Topic ARN, populated at init() from CreateTopic. Operator may pre-set to skip init's CreateTopic call. */\n topicArn?: string;\n}\n\nexport interface InitSettings {\n topicName: string;\n client?: SendClient;\n region?: string;\n config?: SNSClientConfig;\n topicArn?: string;\n}\n\nexport interface Mapping {\n /**\n * Per-event message attributes resolved via mapping. Each attribute value\n * resolves to the SDK's expected `{ DataType, StringValue }` shape; operators\n * write either a literal `{ DataType: 'String', StringValue: '$path' }` value\n * or use `{ value: { DataType: 'String', StringValue: '$user.id' } }` style\n * to drive the StringValue from event data.\n */\n messageAttributes?: WalkerOSMapping.Map;\n /**\n * FIFO group ID for FIFO topics. `Mapping.Value` so operators can write\n * `messageGroupId: 'user.id'` (path) instead of hard-coding a literal.\n */\n messageGroupId?: WalkerOSMapping.Value;\n /** FIFO deduplication ID. `Mapping.Value`, same reasoning as `messageGroupId`. */\n messageDeduplicationId?: WalkerOSMapping.Value;\n}\n\nexport interface Env extends DestinationServer.Env {\n AWS: {\n SNSClient: new (config: SNSClientConfig) => SendClient;\n CreateTopicCommand: CommandConstructor<CreateTopicCommandInput>;\n PublishCommand: CommandConstructor<PublishCommandInput>;\n GetTopicAttributesCommand: CommandConstructor<GetTopicAttributesCommandInput>;\n SubscribeCommand: CommandConstructor<SubscribeCommandInput>;\n STSClient: new (config: STSClientConfig) => SendClient;\n GetCallerIdentityCommand: CommandConstructor<GetCallerIdentityCommandInput>;\n };\n}\n\n/**\n * Provisioning options for `walkeros setup destination.<id>` when targeting SNS.\n * Triggered only by the explicit CLI command. Idempotent, never auto-run.\n *\n * `topicName` lives in Settings (one source of truth for setup AND runtime publish).\n */\nexport interface Setup {\n /** AWS region for the topic. Default: 'eu-central-1'. */\n region?: string;\n /** Display name. Optional. */\n displayName?: string;\n /** FIFO topic with content-based deduplication. Default: false. */\n fifoTopic?: boolean;\n /** KMS key for at-rest encryption. Optional. */\n kmsMasterKeyId?: string;\n /** Tags for cost allocation. Optional. */\n tags?: Record<string, string>;\n /** Subscriptions to create or update on the topic. Each is opt-in. */\n subscriptions?: SetupSubscription[];\n}\n\nexport interface SetupSubscription {\n protocol: 'sqs' | 'lambda' | 'https' | 'http' | 'email' | 'sms';\n endpoint: string;\n rawMessageDelivery?: boolean;\n filterPolicy?: Record<string, unknown>;\n deadLetterTargetArn?: string;\n}\n\nexport type Types = CoreDestination.Types<\n Settings,\n Mapping,\n Env,\n InitSettings,\n Setup\n>;\n\nexport interface Destination extends DestinationServer.Destination<Types> {\n init: DestinationServer.InitFn<Types>;\n}\n\nexport type Config = {\n settings: Settings;\n} & DestinationServer.Config<Types>;\n\nexport type InitFn = DestinationServer.InitFn<Types>;\nexport type PushFn = DestinationServer.PushFn<Types>;\nexport type SetupFn = CoreSetupFn<Config, Env>;\n\n// Local override of PartialConfig to forward the Setup (`U`) type arg.\n// Mirrors the BigQuery destination's pattern.\nexport type PartialConfig = Omit<\n DestinationServer.PartialConfig<Types>,\n 'settings' | 'setup'\n> & {\n settings?: Partial<Settings> | Settings;\n setup?: boolean | Setup;\n};\n\nexport type PushEvents = DestinationServer.PushEvents<Mapping>;\n\nexport type Rule = WalkerOSMapping.Rule<Mapping>;\nexport type Rules = WalkerOSMapping.Rules<Rule>;\n","import type { Destination } from './types';\nimport { getConfig } from './config';\nimport { push } from './push';\nimport { setup } from './setup';\nimport { isAWSEnvironment } from './lib/sns';\n\n// Types\nexport * as DestinationSNS from './types';\n\ninterface CreateTopicResponse {\n TopicArn?: string;\n}\n\nfunction isCreateTopicResponse(v: unknown): v is CreateTopicResponse {\n return typeof v === 'object' && v !== null;\n}\n\nexport const destinationSNS: Destination = {\n type: 'aws-sns',\n\n config: {},\n\n setup,\n\n async init({ config: partial, env, logger, id }) {\n const config = getConfig(partial, env);\n if (!config.settings.client) {\n logger.throw('SNS init: client missing (env not injected)');\n return;\n }\n if (!isAWSEnvironment(env)) {\n logger.throw('SNS init: env.AWS missing');\n return;\n }\n\n // Operator-supplied ARN: skip CreateTopic to avoid the runtime IAM cost.\n if (config.settings.topicArn) return config;\n\n const { client, topicName, region } = config.settings;\n try {\n const res: unknown = await client.send(\n new env.AWS.CreateTopicCommand({ Name: topicName }),\n );\n if (\n !isCreateTopicResponse(res) ||\n typeof res.TopicArn !== 'string' ||\n res.TopicArn.length === 0\n ) {\n logger.throw(\n `SNS init: CreateTopic returned no ARN for \"${topicName}\"`,\n );\n return;\n }\n config.settings.topicArn = res.TopicArn;\n } catch (err) {\n logger.error(\n `SNS init failed for topic \"${topicName}\" in region ${region ?? 'eu-central-1'}. ` +\n `Either set settings.topicArn directly, run \"walkeros setup destination.${id}\", ` +\n `or grant sns:CreateTopic to the runtime role.`,\n {\n topicName,\n region,\n error: err instanceof Error ? err.message : String(err),\n },\n );\n throw err;\n }\n\n return config;\n },\n\n async push(event, context) {\n return await push(event, context);\n },\n};\n\nexport default destinationSNS;\n"],"mappings":"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","names":["types_exports","import_core","import_core","isAWSEnvironment","getConfig","import_core","push","isAWSEnvironment","import_core","types_exports","isCreateTopicResponse","getConfig","isAWSEnvironment","push"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/firehose/config.ts","../src/lib/client.ts","../src/lib/credentials.ts","../src/lib/errors.ts","../src/firehose/lib/firehose.ts","../src/firehose/lib/deliver.ts","../src/lib/payload.ts","../src/firehose/lib/records.ts","../src/firehose/push.ts","../src/firehose/pushBatch.ts","../src/firehose/types/index.ts","../src/firehose/index.ts","../src/sns/lib/sns.ts","../src/sns/config.ts","../src/sns/push.ts","../src/sns/setup.ts","../src/sns/types/index.ts","../src/sns/index.ts"],"sourcesContent":["// AWS Firehose\nexport { destinationFirehose } from './firehose';\nexport * as DestinationFirehose from './firehose/types';\n\n// AWS SNS\nexport { destinationSNS } from './sns';\nexport * as DestinationSNS from './sns/types';\n\n// Default remains Firehose for backward compatibility.\nexport { destinationFirehose as default } from './firehose';\n","import type { Logger } from '@walkeros/core';\nimport { FirehoseClient } from '@aws-sdk/client-firehose';\nimport { isObject } from '@walkeros/core';\nimport type {\n Config,\n CredentialSource,\n Env,\n FirehoseConfig,\n InitSettings,\n PartialConfig,\n Settings,\n} from './types';\nimport {\n DEFAULT_REGION,\n InFlight,\n firstString,\n isHandlerInstance,\n loadSdkRegion,\n} from '../lib/client';\nimport { parseCredentials } from '../lib/credentials';\nimport { configError } from '../lib/errors';\nimport { STREAM_NAME_PATTERN, createClient } from './lib/firehose';\n\n/**\n * Equal as written in a flow: the same value, or plain objects and arrays with\n * equal contents. Anything else (a client, a provider function, a handler\n * instance) is equal only to itself.\n */\nfunction sameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => sameValue(v, b[i]));\n if (!isObject(a) || !isObject(b)) return false;\n if (!isPlain(a) || !isPlain(b)) return false;\n const keys = Object.keys(a);\n return (\n keys.length === Object.keys(b).length &&\n keys.every((key) => key in b && sameValue(a[key], b[key]))\n );\n}\n\nfunction isPlain(value: object): boolean {\n const proto: unknown = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Reads a flat key and its deprecated `settings.firehose` alias. Both set to\n * different values throws: a silent winner could route events to the wrong\n * stream after a half-done migration.\n */\nfunction pick<K extends keyof FirehoseConfig>(\n key: K,\n flat: FirehoseConfig[K],\n alias: FirehoseConfig[K],\n): FirehoseConfig[K] {\n if (flat !== undefined && alias !== undefined && !sameValue(flat, alias))\n throw configError(\n `Firehose: settings.${key} and settings.firehose.${key} are both set and differ. Remove settings.firehose.`,\n );\n return flat ?? alias;\n}\n\n/**\n * Resolves the settings once, offline: no credentials are loaded and no AWS\n * call is made. The caller's config is never mutated.\n */\nexport async function getConfig(\n partialConfig: PartialConfig = {},\n env: Env | undefined,\n logger: Logger.Instance,\n): Promise<Config> {\n const input: InitSettings = partialConfig.settings ?? {};\n const alias: FirehoseConfig = input.firehose ?? {};\n\n const streamName = pick('streamName', input.streamName, alias.streamName);\n const sdkConfig = pick('config', input.config, alias.config);\n const userClient = pick('client', input.client, alias.client);\n const regionSetting = pick('region', input.region, alias.region);\n\n if (!streamName)\n throw configError(\n 'Firehose: settings.streamName is missing. Set it to the name of your Firehose stream.',\n );\n if (!STREAM_NAME_PATTERN.test(streamName))\n throw configError(\n `Firehose: settings.streamName \"${streamName}\" is not a valid stream name. Use 1 to 64 letters, digits, \"_\", \".\" or \"-\".`,\n );\n\n const sdkRegion =\n typeof sdkConfig?.region === 'string' ? sdkConfig.region : undefined;\n const region =\n firstString(regionSetting, sdkRegion, await loadSdkRegion()) ??\n DEFAULT_REGION;\n\n const credentials = parseCredentials(partialConfig.credentials);\n const credentialSource: CredentialSource = userClient\n ? 'client'\n : credentials\n ? 'config.credentials'\n : sdkConfig?.credentials !== undefined\n ? 'settings.config.credentials'\n : 'default chain';\n\n const client =\n userClient ??\n createClient(env?.AWS?.FirehoseClient ?? FirehoseClient, {\n region,\n config: sdkConfig,\n credentials,\n timeout: partialConfig.timeout,\n });\n\n const settings: Settings = {\n streamName,\n region,\n newline: input.newline !== false,\n client,\n runtime: {\n ownsClient: !userClient && !isHandlerInstance(sdkConfig?.requestHandler),\n inflight: new InFlight(),\n credentialSource,\n },\n };\n if (sdkConfig) settings.config = sdkConfig;\n\n logger.debug('Firehose client ready', {\n stream: streamName,\n region,\n credentials: credentialSource,\n });\n\n return { ...partialConfig, settings };\n}\n\n/**\n * Narrows the settings a push receives. Core types them as the slot a user\n * writes, so the fields `init` set are optional there.\n */\nexport function resolveSettings(settings: InitSettings | undefined): Settings {\n const { streamName, region, client, runtime } = settings ?? {};\n if (!streamName || !region || !client || !runtime)\n throw configError('Firehose: the destination was not initialized.');\n return {\n streamName,\n region,\n newline: settings?.newline !== false,\n client,\n runtime,\n };\n}\n","import {\n loadConfig,\n NODE_REGION_CONFIG_FILE_OPTIONS,\n NODE_REGION_CONFIG_OPTIONS,\n} from '@smithy/core/config';\n\n/** Region used when neither the flow nor the environment names one. */\nexport const DEFAULT_REGION = 'eu-central-1';\n\n/** The collector's per-delivery race when `config.timeout` is unset. */\nexport const DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Share of `config.timeout` one SDK attempt may take. The SDK standard retry\n * makes up to three attempts; at a quarter each, three attempts plus the SDK's\n * longest documented waits end before the collector stops waiting.\n */\nexport const REQUEST_TIMEOUT_SHARE = 0.25;\n\n/**\n * The part of an AWS SDK v3 client this package calls: `send` with a command,\n * and `destroy` on a client it created. The SDK clients satisfy it, and so\n * does an injected mock (tests, simulate) without a cast.\n */\nexport interface SendClient {\n send(command: object): Promise<unknown>;\n destroy?(): void;\n}\n\n/**\n * True for a request handler instance (anything with a `handle` method), as\n * opposed to handler options. An instance the user passed belongs to the\n * user: a client closed by this package would close its sockets too.\n */\nexport function isHandlerInstance(handler: unknown): boolean {\n return (\n typeof handler === 'object' &&\n handler !== null &&\n 'handle' in handler &&\n typeof handler.handle === 'function'\n );\n}\n\n/** `requestHandler` options for a client this package builds. */\nexport interface RequestHandlerOptions {\n requestTimeout: number;\n throwOnRequestTimeout: true;\n}\n\n/** The collector's race as this package reads it: positive and finite. */\nexport function resolveTimeout(timeout?: number): number {\n if (timeout === undefined || !Number.isFinite(timeout) || timeout <= 0)\n return DEFAULT_TIMEOUT_MS;\n return timeout;\n}\n\n/**\n * Per-attempt timeout for a client this package builds. Without it the SDK\n * waits on a hung request forever (its default is 0, off), so the retry it\n * keeps never fires and the collector's race is the only bound.\n */\nexport function requestHandlerOptions(timeout?: number): RequestHandlerOptions {\n return {\n requestTimeout: Math.floor(resolveTimeout(timeout) * REQUEST_TIMEOUT_SHARE),\n throwOnRequestTimeout: true,\n };\n}\n\n/** The parts of an SNS topic ARN this package reads. */\nexport interface TopicArn {\n region: string;\n accountId: string;\n name: string;\n}\n\n/** Parses `arn:<partition>:sns:<region>:<account>:<name>`. */\nexport function parseTopicArn(arn: string): TopicArn | undefined {\n const parts = arn.split(':');\n if (parts.length !== 6) return undefined;\n const [prefix, partition, service, region, accountId, name] = parts;\n if (prefix !== 'arn' || !partition || service !== 'sns') return undefined;\n if (!region || !accountId || !name) return undefined;\n return { region, accountId, name };\n}\n\n/**\n * The region the SDK would pick on its own: `AWS_REGION`, else the active\n * profile's `region`. Local files and variables only, never the network.\n * `undefined` when neither names one.\n */\nexport async function loadSdkRegion(): Promise<string | undefined> {\n try {\n const region = await loadConfig(\n NODE_REGION_CONFIG_OPTIONS,\n NODE_REGION_CONFIG_FILE_OPTIONS,\n )();\n return typeof region === 'string' && region.length > 0 ? region : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** The first non-empty string, used for the region order. */\nexport function firstString(\n ...candidates: Array<string | undefined>\n): string | undefined {\n return candidates.find(\n (candidate): candidate is string =>\n typeof candidate === 'string' && candidate.length > 0,\n );\n}\n\n/**\n * Tracks the sends an instance has in flight, so `destroy` can let them land\n * before it closes the client under them.\n */\nexport class InFlight {\n private readonly pending = new Set<Promise<unknown>>();\n\n track<T>(promise: Promise<T>): Promise<T> {\n this.pending.add(promise);\n const remove = () => {\n this.pending.delete(promise);\n };\n promise.then(remove, remove);\n return promise;\n }\n\n get size(): number {\n return this.pending.size;\n }\n\n /** Resolves once every tracked send settled, whatever its outcome. */\n async settle(): Promise<void> {\n while (this.pending.size > 0) {\n await Promise.allSettled([...this.pending]);\n }\n }\n}\n","import { isObject } from '@walkeros/core';\nimport { configError } from './errors';\n\n/**\n * Static AWS keys, the shape `config.credentials` takes. Back each value with\n * `$secret`; a literal key in a flow file is a leaked key.\n */\nexport interface AwsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n}\n\n/** `config.credentials` as written: the object, or the same object as JSON. */\nexport type CredentialsInput = AwsCredentials | string;\n\nfunction nonEmpty(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/**\n * Checks `config.credentials` offline and returns the keys, or `undefined`\n * when the slot is unset (the SDK's default chain then applies).\n *\n * Messages name the field that is wrong and never a value: a credentials\n * error ends up in logs, and the value it would echo is the secret.\n */\nexport function parseCredentials(input: unknown): AwsCredentials | undefined {\n if (input === undefined || input === null || input === '') return undefined;\n\n let value: unknown = input;\n if (typeof input === 'string') {\n try {\n value = JSON.parse(input);\n } catch {\n // The parser's own message quotes the input, so it is not passed on.\n throw configError(\n 'config.credentials is not valid JSON. Expected {\"accessKeyId\", \"secretAccessKey\", \"sessionToken\"?}.',\n );\n }\n }\n\n if (!isObject(value))\n throw configError(\n 'config.credentials must be an object with accessKeyId and secretAccessKey.',\n );\n\n const { accessKeyId, secretAccessKey, sessionToken } = value;\n\n for (const [field, fieldValue] of [\n ['accessKeyId', accessKeyId],\n ['secretAccessKey', secretAccessKey],\n ] as const) {\n if (!nonEmpty(fieldValue))\n throw configError(\n `config.credentials.${field} is missing or empty. Set it from a $secret.`,\n );\n }\n\n if (sessionToken !== undefined && !nonEmpty(sessionToken))\n throw configError(\n 'config.credentials.sessionToken must be a non-empty string when set.',\n );\n\n const credentials: AwsCredentials = {\n accessKeyId: String(accessKeyId),\n secretAccessKey: String(secretAccessKey),\n };\n if (sessionToken !== undefined) credentials.sessionToken = sessionToken;\n return credentials;\n}\n","import {\n isServerError,\n isThrottlingError,\n isTransientError,\n} from '@smithy/core/retry';\n\n/** The AWS service a delivery goes to, as it appears in every message. */\nexport type AwsService = 'Firehose' | 'SNS';\n\n/**\n * What a failure is about: the service, the stream or topic and the region.\n * Every message carries all three, because a wrong region and a wrong name\n * produce the same AWS answer.\n */\nexport interface DeliveryTarget {\n service: AwsService;\n /** Stream name or topic ARN (or topic name before the ARN is known). */\n resource: string;\n region: string;\n /** Destination id, used by the SNS setup hint. */\n id?: string;\n}\n\nexport interface AwsDeliveryErrorOptions {\n code: string;\n status?: number;\n retryable: boolean;\n cause?: unknown;\n}\n\n/**\n * Every failure this package reports or throws. `code` is the AWS error name\n * or one of the package's own codes (`RecordTooLarge`, `InvalidRecord`,\n * `InvalidResponse`, `InvalidConfig`), `status` the HTTP status when AWS\n * answered, and `retryable` whether sending the same record again can succeed.\n * The SDK error stays reachable as `cause`.\n */\nexport class AwsDeliveryError extends Error {\n readonly code: string;\n readonly status: number | undefined;\n readonly retryable: boolean;\n\n constructor(message: string, options: AwsDeliveryErrorOptions) {\n super(message, { cause: options.cause });\n this.name = 'AwsDeliveryError';\n this.code = options.code;\n this.status = options.status;\n this.retryable = options.retryable;\n }\n}\n\n/** Codes whose fix is a configuration change, each with that change. */\nfunction hintFor(code: string, target: DeliveryTarget): string | undefined {\n switch (code) {\n case 'ResourceNotFoundException':\n return target.service === 'Firehose'\n ? 'Check settings.streamName and settings.region.'\n : 'Check settings.topicArn and settings.region.';\n case 'AccessDeniedException':\n case 'AuthorizationErrorException':\n return target.service === 'Firehose'\n ? 'Allow firehose:PutRecordBatch on the stream for these credentials.'\n : 'Allow sns:Publish on the topic for these credentials.';\n case 'UnrecognizedClientException':\n case 'InvalidSignatureException':\n case 'ExpiredTokenException':\n case 'CredentialsProviderError':\n return 'Check config.credentials or the AWS credentials in the environment.';\n case 'NotFoundException':\n return target.service === 'SNS'\n ? `Run \"walkeros setup destination.${target.id ?? '<id>'}\" or set settings.topicArn.`\n : undefined;\n default:\n return undefined;\n }\n}\n\nfunction describe(target: DeliveryTarget): string {\n const kind = target.service === 'Firehose' ? 'stream' : 'topic';\n return `${target.service} ${kind} \"${target.resource}\" (${target.region})`;\n}\n\n/** Builds the message: target, code, the AWS text and, if listed, the fix. */\nexport function formatMessage(\n target: DeliveryTarget,\n code: string,\n text: string,\n): string {\n const hint = hintFor(code, target);\n const base = `${describe(target)}: ${code}: ${text}`;\n return hint ? `${base} ${hint}` : base;\n}\n\n/** The error shape the SDK classifier reads. */\ntype SdkError = Parameters<typeof isTransientError>[0];\n\nfunction isSdkError(error: unknown): error is SdkError {\n return error instanceof Error;\n}\n\nfunction readCode(error: SdkError): string {\n // Network errors from Node arrive as a plain `Error` whose cause is in the\n // errno `code` (ECONNRESET, ETIMEDOUT); the name alone would say nothing.\n if (error.name !== 'Error') return error.name;\n const code: unknown = Reflect.get(error, 'code');\n return typeof code === 'string' && code.length > 0 ? code : error.name;\n}\n\n/**\n * Wraps an error a whole call threw. `retryable` is the SDK's own public\n * classifier, the one its standard retry strategy uses, so this package keeps\n * no table of its own.\n */\nexport function fromSdkError(\n error: unknown,\n target: DeliveryTarget,\n): AwsDeliveryError {\n if (error instanceof AwsDeliveryError) return error;\n\n if (!isSdkError(error)) {\n return new AwsDeliveryError(\n formatMessage(target, 'UnknownError', String(error)),\n { code: 'UnknownError', retryable: false, cause: error },\n );\n }\n\n const code = readCode(error);\n const retryable =\n isThrottlingError(error) || isTransientError(error) || isServerError(error);\n\n return new AwsDeliveryError(formatMessage(target, code, error.message), {\n code,\n status: error.$metadata?.httpStatusCode,\n retryable,\n cause: error,\n });\n}\n\n/**\n * A record AWS rejected inside an accepted call. Both per-record codes AWS\n * documents for PutRecordBatch (`ServiceUnavailableException`,\n * `InternalFailure`) are retryable, so every such rejection is.\n */\nexport function recordError(\n target: DeliveryTarget,\n code: string,\n text: string,\n status?: number,\n): AwsDeliveryError {\n return new AwsDeliveryError(formatMessage(target, code, text), {\n code,\n status,\n retryable: true,\n });\n}\n\n/**\n * A failure this package detected itself, without an AWS answer to classify:\n * a record it refused to send, or a response it could not read.\n */\nexport function localError(\n target: DeliveryTarget,\n code: 'RecordTooLarge' | 'InvalidRecord' | 'InvalidResponse',\n text: string,\n cause?: unknown,\n): AwsDeliveryError {\n return new AwsDeliveryError(formatMessage(target, code, text), {\n code,\n retryable: code === 'InvalidResponse',\n cause,\n });\n}\n\n/** A configuration error found offline in `init` or `setup`. */\nexport function configError(message: string): AwsDeliveryError {\n return new AwsDeliveryError(message, {\n code: 'InvalidConfig',\n retryable: false,\n });\n}\n","import type { FirehoseClientConfig } from '@aws-sdk/client-firehose';\nimport type { AwsCredentials } from '../../lib/credentials';\nimport type { SendClient } from '../../lib/client';\nimport type { FirehoseClientConstructor } from '../types';\nimport { requestHandlerOptions } from '../../lib/client';\n\n/** Firehose stream names: 1 to 64 letters, digits, `_`, `.` or `-`. */\nexport const STREAM_NAME_PATTERN = /^[a-zA-Z0-9_.-]{1,64}$/;\n\nexport interface ClientOptions {\n region: string;\n /** Raw SDK options from `settings.config`. Never mutated. */\n config?: FirehoseClientConfig;\n /** Parsed `config.credentials`; wins over `config.credentials` of the SDK options. */\n credentials?: AwsCredentials;\n /** The collector's race, `config.timeout`. */\n timeout?: number;\n}\n\n/**\n * Builds the client the destination owns. The per-attempt timeout is added\n * only when the user passed no `requestHandler` of their own.\n */\nexport function createClient(\n Client: FirehoseClientConstructor,\n options: ClientOptions,\n): SendClient {\n const clientConfig: FirehoseClientConfig = {\n ...options.config,\n region: options.region,\n };\n if (options.credentials) clientConfig.credentials = options.credentials;\n if (clientConfig.requestHandler === undefined)\n clientConfig.requestHandler = requestHandlerOptions(options.timeout);\n\n return new Client(clientConfig);\n}\n","import type { Destination, Logger, WalkerOS } from '@walkeros/core';\nimport { PutRecordBatchCommand } from '@aws-sdk/client-firehose';\nimport type { Env, Settings } from '../types';\nimport type { DeliveryTarget } from '../../lib/errors';\nimport {\n AwsDeliveryError,\n fromSdkError,\n localError,\n recordError,\n} from '../../lib/errors';\nimport type { PlannedRecord } from './records';\nimport { planChunks, toPayload } from './records';\n\n/** One event to deliver, with what a mapping produced for it. */\nexport interface DeliveryItem {\n event: WalkerOS.Event;\n data?: unknown;\n}\n\n/** A record that did not land, by its index in the delivery. */\nexport interface DeliveryFailure {\n index: number;\n error: AwsDeliveryError;\n}\n\n/** The per-record answer of a `PutRecordBatch` call, as far as it is read. */\ninterface RecordResponse {\n errorCode?: string;\n errorMessage?: string;\n}\n\n/** A `PutRecordBatch` answer, read field by field from an unknown value. */\ninterface CallResponse {\n failedPutCount?: number;\n responses?: RecordResponse[];\n status?: number;\n attempts?: number;\n}\n\n/** Any non-null object, SDK errors included (`isObject` accepts plain ones only). */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction readMetadata(value: unknown): { status?: number; attempts?: number } {\n if (!isRecord(value)) return {};\n const metadata = value.$metadata;\n if (!isRecord(metadata)) return {};\n const { httpStatusCode, attempts } = metadata;\n return {\n status: typeof httpStatusCode === 'number' ? httpStatusCode : undefined,\n attempts: typeof attempts === 'number' ? attempts : undefined,\n };\n}\n\nfunction readResponse(value: unknown): CallResponse {\n const response: CallResponse = readMetadata(value);\n if (!isRecord(value)) return response;\n\n if (typeof value.FailedPutCount === 'number')\n response.failedPutCount = value.FailedPutCount;\n\n if (Array.isArray(value.RequestResponses)) {\n response.responses = value.RequestResponses.map(\n (entry: unknown): RecordResponse => {\n if (!isRecord(entry)) return {};\n const { ErrorCode, ErrorMessage } = entry;\n return {\n errorCode:\n typeof ErrorCode === 'string' && ErrorCode.length > 0\n ? ErrorCode\n : undefined,\n errorMessage:\n typeof ErrorMessage === 'string' ? ErrorMessage : undefined,\n };\n },\n );\n }\n\n return response;\n}\n\n/**\n * Maps one call's answer onto its records. Per-record answers are trusted only\n * when they line up with the records sent and agree with `FailedPutCount`; a\n * reported failure they cannot place fails the whole chunk as\n * `InvalidResponse`, never as delivered.\n */\nfunction chunkFailures(\n chunk: readonly PlannedRecord[],\n response: CallResponse,\n target: DeliveryTarget,\n): DeliveryFailure[] {\n const { responses, failedPutCount, status } = response;\n\n if (responses && responses.length === chunk.length) {\n const failures: DeliveryFailure[] = [];\n responses.forEach((entry, local) => {\n if (!entry.errorCode) return;\n failures.push({\n index: chunk[local].index,\n error: recordError(\n target,\n entry.errorCode,\n entry.errorMessage ?? 'Record rejected.',\n status,\n ),\n });\n });\n // Entries that do not account for the reported count cannot place the\n // failures, so none of them is trusted.\n if (failedPutCount === undefined || failures.length === failedPutCount)\n return failures;\n } else if (failedPutCount === 0) return [];\n\n const error = localError(\n target,\n 'InvalidResponse',\n `PutRecordBatch answered without per-record results that match its FailedPutCount for ${chunk.length} records (FailedPutCount ${failedPutCount ?? 'missing'}).`,\n );\n return chunk.map(({ index }) => ({ index, error }));\n}\n\n/**\n * Sends records to the stream and says which did not land.\n *\n * `push` and `pushBatch` both arrive here. Each chunk is sent once, through\n * the SDK and its own retry, and all chunks go out together, so one failed\n * call never stops the rest. The answer lists every failed record by index,\n * sorted; an empty list means everything landed.\n */\nexport async function deliver(\n items: readonly DeliveryItem[],\n settings: Settings,\n env: Env | undefined,\n logger: Logger.Instance,\n): Promise<DeliveryFailure[]> {\n const target: DeliveryTarget = {\n service: 'Firehose',\n resource: settings.streamName,\n region: settings.region,\n };\n const Command = env?.AWS?.PutRecordBatchCommand ?? PutRecordBatchCommand;\n\n const plan = planChunks(\n items.map(({ event, data }) => toPayload(event, data)),\n settings.newline,\n );\n\n const failures: DeliveryFailure[] = plan.refused.map(\n ({ index, code, detail, cause }) => ({\n index,\n error: localError(target, code, detail, cause),\n }),\n );\n\n const sendChunk = async (\n chunk: PlannedRecord[],\n ): Promise<DeliveryFailure[]> => {\n const started = Date.now();\n const bytes = chunk.reduce(\n (sum, record) => sum + record.data.byteLength,\n 0,\n );\n const command = new Command({\n DeliveryStreamName: settings.streamName,\n Records: chunk.map(({ data }) => ({ Data: data })),\n });\n\n let result: DeliveryFailure[];\n let attempts: number | undefined;\n try {\n const raw = await settings.runtime.inflight.track(\n settings.client.send(command),\n );\n const response = readResponse(raw);\n attempts = response.attempts;\n result = chunkFailures(chunk, response, target);\n } catch (thrown) {\n attempts = readMetadata(thrown).attempts;\n const error = fromSdkError(thrown, target);\n result = chunk.map(({ index }) => ({ index, error }));\n }\n\n logger.debug('Firehose PutRecordBatch', {\n stream: settings.streamName,\n records: chunk.length,\n bytes,\n failed: result.length,\n attempts,\n ms: Date.now() - started,\n });\n\n return result;\n };\n\n const results = await Promise.all(plan.chunks.map(sendChunk));\n for (const result of results) failures.push(...result);\n\n return failures.sort((a, b) => a.index - b.index);\n}\n\n/**\n * The error thrown when nothing landed. One shared cause is thrown as is;\n * mixed causes give the most frequent code, retryable only if every record\n * was, with the counts per code in the message.\n */\nexport function nothingLanded(\n failures: readonly DeliveryFailure[],\n): AwsDeliveryError {\n const first = failures[0].error;\n if (failures.every(({ error }) => error === first)) return first;\n\n const byCode = new Map<string, { count: number; sample: AwsDeliveryError }>();\n for (const { error } of failures) {\n const entry = byCode.get(error.code);\n if (entry) entry.count += 1;\n else byCode.set(error.code, { count: 1, sample: error });\n }\n\n let dominant = { count: 0, sample: first };\n for (const entry of byCode.values())\n if (entry.count > dominant.count) dominant = entry;\n\n const counts = [...byCode.entries()]\n .map(([code, { count }]) => `${count} ${code}`)\n .join(', ');\n\n return new AwsDeliveryError(\n `${dominant.sample.message} (all ${failures.length} records failed: ${counts})`,\n {\n code: dominant.sample.code,\n status: dominant.sample.status,\n retryable: failures.every(({ error }) => error.retryable),\n cause: dominant.sample,\n },\n );\n}\n\n/**\n * The collector's reading of a delivery: resolve when everything landed,\n * the failed indices when some did, a throw when none did.\n */\nexport function toOutcome(\n total: number,\n failures: readonly DeliveryFailure[],\n): void | Destination.BatchOutcome {\n if (failures.length === 0) return;\n if (failures.length >= total) throw nothingLanded(failures);\n return {\n failed: failures.map(({ index, error }) => ({ index, error })),\n };\n}\n","import type { WalkerOS } from '@walkeros/core';\nimport { isObject } from '@walkeros/core';\n\n/**\n * What one record or message carries: the mapped `data` when the mapping\n * produced an object, else the full event.\n */\nexport function toPayload(event: WalkerOS.Event, data?: unknown): object {\n return isObject(data) ? data : event;\n}\n","export { toPayload } from '../../lib/payload';\n\n/** Most records one `PutRecordBatch` call accepts. */\nexport const MAX_RECORDS_PER_CALL = 500;\n\n/**\n * Raw bytes per call, kept below the documented 4 MiB request limit with room\n * for the base64 encoding and the JSON envelope of the request.\n */\nexport const MAX_BYTES_PER_CALL = 2_900_000;\n\n/** Largest record Firehose accepts, newline included. */\nexport const MAX_RECORD_BYTES = 1_000_000;\n\n/** The record bytes: the payload as JSON, plus `\\n` unless `newline` is false. */\nexport function toRecordData(payload: unknown, newline = true): Buffer {\n const json = JSON.stringify(payload);\n if (typeof json !== 'string')\n throw new TypeError('The payload does not serialize to JSON.');\n return Buffer.from(newline ? `${json}\\n` : json);\n}\n\n/** One record on its way out, with its index in the delivery. */\nexport interface PlannedRecord {\n index: number;\n data: Buffer;\n}\n\n/** A record refused before sending; it is never sent. */\nexport interface RefusedRecord {\n index: number;\n code: 'RecordTooLarge' | 'InvalidRecord';\n detail: string;\n cause?: unknown;\n}\n\nexport interface Plan {\n chunks: PlannedRecord[][];\n refused: RefusedRecord[];\n}\n\n/**\n * Turns payloads into records and splits them into calls of at most\n * `MAX_RECORDS_PER_CALL` records and `MAX_BYTES_PER_CALL` bytes, in order.\n * A record that cannot be serialized or is too large is refused on its own,\n * so it never fails its neighbours.\n */\nexport function planChunks(\n payloads: readonly unknown[],\n newline: boolean,\n): Plan {\n const chunks: PlannedRecord[][] = [];\n const refused: RefusedRecord[] = [];\n let current: PlannedRecord[] = [];\n let currentBytes = 0;\n\n payloads.forEach((payload, index) => {\n let data: Buffer;\n try {\n data = toRecordData(payload, newline);\n } catch (error) {\n refused.push({\n index,\n code: 'InvalidRecord',\n detail: `Record ${index} does not serialize to JSON: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n });\n return;\n }\n\n if (data.byteLength > MAX_RECORD_BYTES) {\n refused.push({\n index,\n code: 'RecordTooLarge',\n detail: `Record ${index} is ${data.byteLength} bytes, over the ${MAX_RECORD_BYTES} byte limit.`,\n });\n return;\n }\n\n if (\n current.length >= MAX_RECORDS_PER_CALL ||\n currentBytes + data.byteLength > MAX_BYTES_PER_CALL\n ) {\n chunks.push(current);\n current = [];\n currentBytes = 0;\n }\n\n current.push({ index, data });\n currentBytes += data.byteLength;\n });\n\n if (current.length > 0) chunks.push(current);\n return { chunks, refused };\n}\n","import type { PushFn } from './types';\nimport { resolveSettings } from './config';\nimport { deliver } from './lib/deliver';\n\n/** Sends one event as one record. Resolves once AWS confirmed it, else throws. */\nexport const push: PushFn = async function (event, context) {\n const { config, data, env, logger } = context;\n const settings = resolveSettings(config.settings);\n\n const [failure] = await deliver([{ event, data }], settings, env, logger);\n if (failure) throw failure.error;\n};\n","import type { PushBatchFn } from './types';\nimport { resolveSettings } from './config';\nimport { deliver, toOutcome } from './lib/deliver';\n\n/**\n * Sends a flushed batch. Resolves when every record landed, returns the\n * failed indices when some did, and throws when none did.\n */\nexport const pushBatch: PushBatchFn = async (batch, context) => {\n const { config, env, logger } = context;\n if (batch.entries.length === 0) return;\n\n const settings = resolveSettings(config.settings);\n const failures = await deliver(\n batch.entries.map(({ event, data }) => ({ event, data })),\n settings,\n env,\n logger,\n );\n\n return toOutcome(batch.entries.length, failures);\n};\n","import type { DestinationServer } from '@walkeros/server-core';\nimport type {\n Mapping as WalkerOSMapping,\n Destination as CoreDestination,\n} from '@walkeros/core';\nimport type {\n FirehoseClientConfig,\n PutRecordBatchCommandInput,\n} from '@aws-sdk/client-firehose';\nimport type { InFlight, SendClient } from '../../lib/client';\nimport type { CredentialsInput } from '../../lib/credentials';\n\nexport type { SendClient } from '../../lib/client';\n\n/** Client and stream options, as the deprecated `settings.firehose` held them. */\nexport interface FirehoseConfig {\n streamName?: string;\n client?: SendClient;\n region?: string;\n config?: FirehoseClientConfig;\n}\n\n/**\n * Settings as a user writes them. Only `streamName` is required (here or in\n * the deprecated `firehose` alias); `init` fills in the rest.\n */\nexport interface InitSettings {\n /** Name of the Firehose stream. */\n streamName?: string;\n /** AWS region. Default: `AWS_REGION` or the profile, else `eu-central-1`. */\n region?: string;\n /** Append `\\n` to every record. Default `true`. */\n newline?: boolean;\n /** Raw `FirehoseClient` options, passed through to the SDK. */\n config?: FirehoseClientConfig;\n /** A client of your own. Used as is and never closed by the destination. */\n client?: SendClient;\n /** @deprecated Use the flat fields. Read at `init` as an alias. */\n firehose?: FirehoseConfig;\n /** Runtime-only, set by `init`: what the destination owns. Not user-facing. */\n runtime?: Runtime;\n}\n\n/** What `init` resolved. Every field is present. */\nexport interface Settings {\n streamName: string;\n region: string;\n newline: boolean;\n config?: FirehoseClientConfig;\n client: SendClient;\n runtime: Runtime;\n}\n\n/** Runtime-only state of one initialized instance. */\nexport interface Runtime {\n /**\n * True when the destination built the client from nothing the user owns,\n * and so closes it. A user client, or a user request handler instance in\n * `settings.config`, keeps it false.\n */\n ownsClient: boolean;\n /** Sends in flight, awaited by `destroy`. */\n inflight: InFlight;\n /** Where the client's credentials come from, for the debug line. */\n credentialSource: CredentialSource;\n}\n\nexport type CredentialSource =\n | 'config.credentials'\n | 'settings.config.credentials'\n | 'default chain'\n | 'client';\n\nexport interface Mapping {}\n\nexport type FirehoseClientConstructor = new (\n config: FirehoseClientConfig,\n) => SendClient;\n\nexport type PutRecordBatchCommandConstructor = new (\n input: PutRecordBatchCommandInput,\n) => { readonly input: PutRecordBatchCommandInput };\n\n/**\n * Optional SDK overrides. Each one present wins over the SDK the package\n * imports; tests and simulate inject mocks here.\n */\nexport interface Env extends DestinationServer.Env {\n AWS?: {\n FirehoseClient?: FirehoseClientConstructor;\n PutRecordBatchCommand?: PutRecordBatchCommandConstructor;\n };\n}\n\nexport type Types = CoreDestination.Types<\n Settings,\n Mapping,\n Env,\n InitSettings,\n unknown,\n CredentialsInput\n>;\n\nexport interface Destination extends DestinationServer.Destination<Types> {\n init: InitFn;\n pushBatch: PushBatchFn;\n}\n\nexport type Config = {\n settings: InitSettings;\n} & DestinationServer.Config<Types>;\n\nexport type InitFn = DestinationServer.InitFn<Types>;\nexport type PushFn = DestinationServer.PushFn<Types>;\nexport type PushBatchFn = CoreDestination.PushBatchFn<Types>;\n\nexport type PartialConfig = DestinationServer.PartialConfig<Types>;\n\nexport type PushEvents = DestinationServer.PushEvents<Mapping>;\n\nexport type Rule = WalkerOSMapping.Rule<Mapping>;\nexport type Rules = WalkerOSMapping.Rules<Rule>;\n","import type { Destination } from './types';\nimport { getConfig } from './config';\nimport { push } from './push';\nimport { pushBatch } from './pushBatch';\n\n// Types\nexport * as DestinationFirehose from './types';\n\n/**\n * The Firehose destination. Batching is on by default: a flushed batch goes\n * out as `PutRecordBatch` calls of at most 500 records.\n */\nexport const destinationFirehose: Destination = {\n type: 'aws-firehose',\n\n config: { batch: { size: 500, age: 1000 } },\n\n async init({ config, env, logger }) {\n return getConfig(config, env, logger);\n },\n\n push,\n\n pushBatch,\n\n async destroy({ config, logger }) {\n const runtime = config.settings?.runtime;\n if (!runtime) return;\n\n // The collector awaits only the last flush per buffer, so earlier sends\n // can still be on the wire. Closing the client would cut them off.\n await runtime.inflight.settle();\n\n if (!runtime.ownsClient) return;\n runtime.ownsClient = false;\n config.settings?.client?.destroy?.();\n logger.debug('Firehose client closed');\n },\n};\n\nexport default destinationFirehose;\n","import type { SNSClientConfig } from '@aws-sdk/client-sns';\nimport type { STSClientConfig } from '@aws-sdk/client-sts';\nimport { SNSClient } from '@aws-sdk/client-sns';\nimport { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';\nimport { isObject } from '@walkeros/core';\nimport type { AwsCredentials } from '../../lib/credentials';\nimport type { SendClient } from '../../lib/client';\nimport type { DeliveryTarget } from '../../lib/errors';\nimport type { Env } from '../types';\nimport { isHandlerInstance, requestHandlerOptions } from '../../lib/client';\nimport { fromSdkError } from '../../lib/errors';\n\nexport interface ClientOptions {\n region: string;\n /** Raw SDK options from `settings.config`. Never mutated. */\n config?: SNSClientConfig;\n /** Parsed `config.credentials`; wins over the credentials of the SDK options. */\n credentials?: AwsCredentials;\n /** The collector's race, `config.timeout`. */\n timeout?: number;\n}\n\n/**\n * The options of a client this package builds: the region, the credentials\n * slot, and the per-attempt timeout unless the user passed a handler.\n */\nexport function clientConfig(options: ClientOptions): SNSClientConfig {\n const resolved: SNSClientConfig = {\n ...options.config,\n region: options.region,\n };\n if (options.credentials) resolved.credentials = options.credentials;\n if (resolved.requestHandler === undefined)\n resolved.requestHandler = requestHandlerOptions(options.timeout);\n return resolved;\n}\n\n/** Builds the SNS client the destination owns, from env or the SDK. */\nexport function createSnsClient(\n env: Env | undefined,\n options: ClientOptions,\n): SendClient {\n const Client = env?.AWS?.SNSClient ?? SNSClient;\n return new Client(clientConfig(options));\n}\n\n/**\n * Builds the STS client for the account lookup, from env or the SDK. It shares\n * the region and credentials of the SNS client, never its endpoint (an SNS\n * endpoint does not answer STS) and never a user's handler instance (the\n * lookup client is closed after one call).\n *\n * One attempt only: the lookup runs before the first publish of a\n * `topicName`-only config, and one attempt of a quarter of `config.timeout`\n * plus the publish's own three still end inside the collector's race. A\n * failed lookup is not cached, so the next publish asks again.\n */\nexport function createStsClient(\n env: Env | undefined,\n options: ClientOptions,\n): SendClient {\n const Client = env?.AWS?.STSClient ?? STSClient;\n const config: STSClientConfig = { region: options.region };\n const credentials = options.credentials ?? options.config?.credentials;\n if (credentials) config.credentials = credentials;\n const userHandler = options.config?.requestHandler;\n config.requestHandler =\n userHandler === undefined || isHandlerInstance(userHandler)\n ? requestHandlerOptions(options.timeout)\n : userHandler;\n config.maxAttempts = 1;\n return new Client(config);\n}\n\n/** Account id and partition of the caller, from `GetCallerIdentity`. */\nexport interface CallerIdentity {\n accountId: string;\n partition: string;\n}\n\n/** Asks STS who the credentials belong to. One call, no retry of its own. */\nexport async function getCallerIdentity(\n client: SendClient,\n env: Env | undefined,\n target: DeliveryTarget,\n): Promise<CallerIdentity> {\n const Command =\n env?.AWS?.GetCallerIdentityCommand ?? GetCallerIdentityCommand;\n let response: unknown;\n try {\n response = await client.send(new Command({}));\n } catch (error) {\n throw fromSdkError(error, target);\n }\n\n if (!isObject(response) || typeof response.Account !== 'string')\n throw fromSdkError(\n new Error('STS GetCallerIdentity returned no Account.'),\n target,\n );\n\n // The caller ARN carries the partition (aws, aws-cn, aws-us-gov).\n const arn = typeof response.Arn === 'string' ? response.Arn : '';\n const partition = arn.split(':')[1] || 'aws';\n return { accountId: response.Account, partition };\n}\n","import type { Logger } from '@walkeros/core';\nimport type {\n Config,\n Env,\n InitSettings,\n PartialConfig,\n Runtime,\n Settings,\n} from './types';\nimport type { DeliveryTarget } from '../lib/errors';\nimport {\n DEFAULT_REGION,\n InFlight,\n firstString,\n isHandlerInstance,\n loadSdkRegion,\n parseTopicArn,\n} from '../lib/client';\nimport { parseCredentials } from '../lib/credentials';\nimport { configError } from '../lib/errors';\nimport { createSnsClient, createStsClient, getCallerIdentity } from './lib/sns';\n\n/**\n * Resolves the settings once, offline: no credentials are loaded, no topic is\n * created and no AWS call is made. The caller's config is never mutated.\n */\nexport async function getConfig(\n partialConfig: PartialConfig = {},\n env: Env | undefined,\n logger: Logger.Instance,\n id: string,\n): Promise<Config> {\n const input: InitSettings = partialConfig.settings ?? {};\n const { topicArn, config: sdkConfig, client: userClient } = input;\n\n const arn = topicArn ? parseTopicArn(topicArn) : undefined;\n if (topicArn && !arn)\n throw configError(\n `SNS: settings.topicArn \"${topicArn}\" is not an SNS topic ARN (arn:aws:sns:<region>:<account>:<name>).`,\n );\n if (!arn && !input.topicName)\n throw configError(\n 'SNS: settings.topicArn is missing. Set it to the ARN of your topic (or set settings.topicName).',\n );\n if (arn && input.region && input.region !== arn.region)\n throw configError(\n `SNS: settings.region \"${input.region}\" differs from the region \"${arn.region}\" in settings.topicArn. Remove settings.region.`,\n );\n if (arn && input.topicName && input.topicName !== arn.name)\n throw configError(\n `SNS: settings.topicName \"${input.topicName}\" differs from the name \"${arn.name}\" in settings.topicArn. Remove settings.topicName.`,\n );\n\n const sdkRegion =\n typeof sdkConfig?.region === 'string' ? sdkConfig.region : undefined;\n const region =\n firstString(input.region, arn?.region, sdkRegion, await loadSdkRegion()) ??\n DEFAULT_REGION;\n const topicName = input.topicName ?? arn?.name;\n\n if (partialConfig.batch !== undefined)\n logger.warn(\n 'SNS publishes one message per event and does not batch. Remove config.batch from this destination.',\n );\n\n const credentials = parseCredentials(partialConfig.credentials);\n const clientOptions = {\n region,\n config: sdkConfig,\n credentials,\n timeout: partialConfig.timeout,\n };\n const client = userClient ?? createSnsClient(env, clientOptions);\n\n const runtime: Runtime = {\n ownsClient: !userClient && !isHandlerInstance(sdkConfig?.requestHandler),\n inflight: new InFlight(),\n topicArn: topicArnResolver(\n topicArn,\n topicName,\n {\n service: 'SNS',\n resource: topicName ?? '',\n region,\n id,\n },\n () => createStsClient(env, clientOptions),\n env,\n ),\n };\n\n const settings: Settings = { region, client, runtime };\n if (topicArn) settings.topicArn = topicArn;\n if (topicName) settings.topicName = topicName;\n if (sdkConfig) settings.config = sdkConfig;\n\n logger.debug('SNS client ready', {\n topic: topicArn ?? topicName,\n region,\n credentials: userClient\n ? 'client'\n : credentials\n ? 'config.credentials'\n : sdkConfig?.credentials !== undefined\n ? 'settings.config.credentials'\n : 'default chain',\n });\n\n return { ...partialConfig, settings };\n}\n\n/**\n * The topic ARN, as a function a publish awaits. A configured ARN is returned\n * as is. A name alone is completed once with the account id from STS, at the\n * first publish, and cached; a failed lookup is not cached, so the next\n * publish asks again.\n */\nfunction topicArnResolver(\n topicArn: string | undefined,\n topicName: string | undefined,\n target: DeliveryTarget,\n stsClient: () => ReturnType<typeof createStsClient>,\n env: Env | undefined,\n): () => Promise<string> {\n if (topicArn) return async () => topicArn;\n\n const lookup = async (): Promise<string> => {\n const client = stsClient();\n try {\n const { accountId, partition } = await getCallerIdentity(\n client,\n env,\n target,\n );\n return `arn:${partition}:sns:${target.region}:${accountId}:${topicName}`;\n } finally {\n client.destroy?.();\n }\n };\n\n let pending: Promise<string> | undefined;\n return () => {\n pending ??= lookup().catch((error: unknown) => {\n pending = undefined;\n throw error;\n });\n return pending;\n };\n}\n\n/**\n * Narrows the settings a push receives. Core types them as the slot a user\n * writes, so the fields `init` set are optional there.\n */\nexport function resolveSettings(settings: InitSettings | undefined): Settings {\n const { region, client, runtime } = settings ?? {};\n if (!region || !client || !runtime)\n throw configError('SNS: the destination was not initialized.');\n return {\n topicArn: settings?.topicArn,\n topicName: settings?.topicName,\n region,\n client,\n runtime,\n };\n}\n","import type { PushFn, Mapping } from './types';\nimport type { Mapping as WalkerOSMapping, Collector } from '@walkeros/core';\nimport { PublishCommand } from '@aws-sdk/client-sns';\nimport { getMappingValue, isObject } from '@walkeros/core';\nimport { resolveSettings } from './config';\nimport type { DeliveryTarget } from '../lib/errors';\nimport { fromSdkError, localError } from '../lib/errors';\nimport { toPayload } from '../lib/payload';\n\ninterface MessageAttribute {\n DataType: string;\n StringValue?: string;\n BinaryValue?: Uint8Array;\n}\n\nfunction isMessageAttribute(value: unknown): value is MessageAttribute {\n return isObject(value) && typeof value.DataType === 'string';\n}\n\nasync function resolveMessageAttributes(\n event: Parameters<PushFn>[0],\n source: WalkerOSMapping.Map | undefined,\n collector: Collector.Instance,\n): Promise<Record<string, MessageAttribute> | undefined> {\n if (!source) return undefined;\n const result: Record<string, MessageAttribute> = {};\n let touched = false;\n for (const [key, value] of Object.entries(source)) {\n const resolved = await getMappingValue(event, value, { collector });\n if (isMessageAttribute(resolved)) {\n result[key] = resolved;\n touched = true;\n } else if (typeof resolved === 'string' && resolved.length > 0) {\n // Bare-string convenience: wrap into String DataType.\n result[key] = { DataType: 'String', StringValue: resolved };\n touched = true;\n } else if (resolved !== undefined && resolved !== null) {\n result[key] = { DataType: 'String', StringValue: String(resolved) };\n touched = true;\n }\n }\n return touched ? result : undefined;\n}\n\nfunction isMapping(value: unknown): value is Mapping {\n return typeof value === 'object' && value !== null;\n}\n\nasync function resolveStringMappingValue(\n event: Parameters<PushFn>[0],\n value: WalkerOSMapping.Value,\n collector: Collector.Instance,\n): Promise<string | undefined> {\n const resolved = await getMappingValue(event, value, { collector });\n if (typeof resolved === 'string' && resolved.length > 0) return resolved;\n if (resolved === undefined || resolved === null) return undefined;\n return String(resolved);\n}\n\n/**\n * Publishes one event. Resolves once SNS accepted the message, else throws an\n * `AwsDeliveryError`; the collector logs it, so SNS logs no error of its own.\n */\nexport const push: PushFn = async function (event, context) {\n const { config, data, env, rule, collector, logger, id } = context;\n const settings = resolveSettings(config.settings);\n const { runtime } = settings;\n\n // Tracked, so destroy waits for a first-publish account lookup too.\n const topicArn = await runtime.inflight.track(runtime.topicArn());\n const target: DeliveryTarget = {\n service: 'SNS',\n resource: topicArn,\n region: settings.region,\n id,\n };\n\n const ruleSettings: Mapping = isMapping(rule?.settings) ? rule.settings : {};\n\n const messageGroupId =\n ruleSettings.messageGroupId !== undefined\n ? await resolveStringMappingValue(\n event,\n ruleSettings.messageGroupId,\n collector,\n )\n : undefined;\n\n const fifo = topicArn.endsWith('.fifo');\n const mappedDeduplicationId =\n ruleSettings.messageDeduplicationId !== undefined\n ? await resolveStringMappingValue(\n event,\n ruleSettings.messageDeduplicationId,\n collector,\n )\n : undefined;\n // The event id is the dedup key everywhere, so a FIFO topic gets it unless\n // a rule maps its own.\n const messageDeduplicationId =\n mappedDeduplicationId ?? (fifo && event.id ? event.id : undefined);\n\n const messageAttributes = await resolveMessageAttributes(\n event,\n ruleSettings.messageAttributes,\n collector,\n );\n\n let message: string;\n try {\n message = JSON.stringify(toPayload(event, data));\n } catch (error) {\n throw localError(\n target,\n 'InvalidRecord',\n `The message does not serialize to JSON: ${error instanceof Error ? error.message : String(error)}`,\n error,\n );\n }\n\n const input: {\n TopicArn: string;\n Message: string;\n MessageGroupId?: string;\n MessageDeduplicationId?: string;\n MessageAttributes?: Record<string, MessageAttribute>;\n } = { TopicArn: topicArn, Message: message };\n if (messageGroupId) input.MessageGroupId = messageGroupId;\n if (messageDeduplicationId)\n input.MessageDeduplicationId = messageDeduplicationId;\n if (messageAttributes) input.MessageAttributes = messageAttributes;\n\n const Command = env?.AWS?.PublishCommand ?? PublishCommand;\n const started = Date.now();\n let attempts: number | undefined;\n try {\n const response = await runtime.inflight.track(\n settings.client.send(new Command(input)),\n );\n attempts = readAttempts(response);\n } catch (error) {\n attempts = readAttempts(error);\n throw fromSdkError(error, target);\n } finally {\n logger.debug('SNS Publish', {\n topicArn,\n fifo,\n event: event.name,\n attempts,\n ms: Date.now() - started,\n });\n }\n};\n\nfunction readAttempts(value: unknown): number | undefined {\n if (typeof value !== 'object' || value === null || !('$metadata' in value))\n return undefined;\n const metadata = value.$metadata;\n if (typeof metadata !== 'object' || metadata === null) return undefined;\n return 'attempts' in metadata && typeof metadata.attempts === 'number'\n ? metadata.attempts\n : undefined;\n}\n","import type { DestinationServer } from '@walkeros/server-core';\nimport type { Env, SendClient, Setup, SetupSubscription, Types } from './types';\nimport type { LifecycleContext } from '@walkeros/core';\nimport {\n CreateTopicCommand,\n GetTopicAttributesCommand,\n SubscribeCommand,\n} from '@aws-sdk/client-sns';\nimport { resolveSetup } from '@walkeros/core';\nimport {\n DEFAULT_REGION,\n firstString,\n isHandlerInstance,\n loadSdkRegion,\n parseTopicArn,\n} from '../lib/client';\nimport { parseCredentials } from '../lib/credentials';\nimport type { DeliveryTarget } from '../lib/errors';\nimport type { CallerIdentity, ClientOptions } from './lib/sns';\nimport { createSnsClient, createStsClient, getCallerIdentity } from './lib/sns';\n\n// Setup is wired to the destination's `setup` slot which uses the broader\n// `DestinationServer.Config<Types>` (settings is optional). We runtime-narrow\n// instead of using the local Config alias so the assignment in index.ts\n// type-checks without contravariance issues.\ntype WideConfig = DestinationServer.Config<Types>;\n\n// No region default here: an unset `setup.region` falls through to the\n// runtime region order, so setup and publish target the same region.\nexport const DEFAULT_SETUP: Required<Pick<Setup, 'fifoTopic'>> & Setup = {\n fifoTopic: false,\n};\n\nexport interface SetupResult {\n topicArn: string;\n /** True when the GetTopicAttributes probe returned 404 (topic absent before this run). */\n topicCreated: boolean;\n tagsApplied: number;\n subscriptionsCreated: number;\n}\n\n// Module-level caller-identity cache so re-running setup in the same process\n// is a no-op for the STS GetCallerIdentity call. Keyed by region (in case\n// future versions support cross-region setup in one process).\nconst identityCache: Map<string, CallerIdentity> = new Map();\n\n/** Test-only: clear the caller-identity cache between test cases. */\nexport function __resetAccountIdCache(): void {\n identityCache.clear();\n}\n\ninterface CreateTopicResponse {\n TopicArn?: string;\n}\n\ninterface AwsErrorMeta {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n}\n\nfunction isAwsError(err: unknown): err is AwsErrorMeta {\n return typeof err === 'object' && err !== null;\n}\n\nfunction isNotFound(err: unknown): boolean {\n if (!isAwsError(err)) return false;\n if (err.name === 'NotFoundException' || err.name === 'NotFound') return true;\n if (err.$metadata && err.$metadata.httpStatusCode === 404) return true;\n return false;\n}\n\nfunction resolveTopicName(name: string, fifo: boolean): string {\n if (fifo) {\n if (name.endsWith('.fifo')) return name;\n return `${name}.fifo`;\n }\n if (name.endsWith('.fifo')) {\n throw new Error(\n `FIFO suffix '.fifo' on standard topic name '${name}'. Set setup.fifoTopic: true or rename the topic.`,\n );\n }\n return name;\n}\n\n/**\n * Account id and partition of the setup credentials, from STS once per region.\n * The partition (aws, aws-cn, aws-us-gov) comes from the caller ARN, so the\n * candidate topic ARN is right outside the standard partition too.\n */\nasync function getIdentity(\n env: Env | undefined,\n options: ClientOptions,\n target: DeliveryTarget,\n): Promise<CallerIdentity> {\n const cached = identityCache.get(options.region);\n if (cached) return cached;\n const stsClient = createStsClient(env, options);\n try {\n const identity = await getCallerIdentity(stsClient, env, target);\n identityCache.set(options.region, identity);\n return identity;\n } finally {\n stsClient.destroy?.();\n }\n}\n\nfunction isCreateTopicResponse(v: unknown): v is CreateTopicResponse {\n return typeof v === 'object' && v !== null;\n}\n\nfunction extractTopicArn(res: unknown): string {\n if (isCreateTopicResponse(res) && typeof res.TopicArn === 'string')\n return res.TopicArn;\n throw new Error('SNS CreateTopic returned no TopicArn');\n}\n\nfunction buildSubscribeAttributes(\n sub: SetupSubscription,\n): Record<string, string> | undefined {\n const attrs: Record<string, string> = {};\n if (sub.rawMessageDelivery !== undefined) {\n attrs.RawMessageDelivery = String(sub.rawMessageDelivery);\n }\n if (sub.filterPolicy !== undefined) {\n attrs.FilterPolicy = JSON.stringify(sub.filterPolicy);\n }\n if (sub.deadLetterTargetArn !== undefined) {\n attrs.RedrivePolicy = JSON.stringify({\n deadLetterTargetArn: sub.deadLetterTargetArn,\n });\n }\n return Object.keys(attrs).length > 0 ? attrs : undefined;\n}\n\nasync function applyDeclaredSubscriptions(\n client: SendClient,\n env: Env | undefined,\n topicArn: string,\n declared: SetupSubscription[],\n): Promise<number> {\n let count = 0;\n for (const sub of declared) {\n const input: {\n TopicArn: string;\n Protocol: string;\n Endpoint: string;\n Attributes?: Record<string, string>;\n } = {\n TopicArn: topicArn,\n Protocol: sub.protocol,\n Endpoint: sub.endpoint,\n };\n const attrs = buildSubscribeAttributes(sub);\n if (attrs) input.Attributes = attrs;\n const Command = env?.AWS?.SubscribeCommand ?? SubscribeCommand;\n await client.send(new Command(input));\n count += 1;\n }\n return count;\n}\n\nexport async function setup(\n ctx: LifecycleContext<WideConfig, Env | undefined>,\n): Promise<SetupResult | undefined> {\n const { config, env, logger } = ctx;\n const merged = resolveSetup(config.setup, DEFAULT_SETUP);\n if (!merged) {\n logger.debug('setup: skipped (config.setup is false or unset)');\n return;\n }\n\n const settings = config.settings ?? {};\n const arn = settings.topicArn ? parseTopicArn(settings.topicArn) : undefined;\n const topicName = settings.topicName ?? arn?.name;\n if (!topicName) {\n logger.throw(\n 'setup: settings.topicName (or settings.topicArn) is required. There is no safe default for the SNS topic name.',\n );\n return;\n }\n\n const sdkRegion =\n typeof settings.config?.region === 'string'\n ? settings.config.region\n : undefined;\n const region =\n firstString(\n merged.region,\n settings.region,\n arn?.region,\n sdkRegion,\n await loadSdkRegion(),\n ) ?? DEFAULT_REGION;\n const fifo = merged.fifoTopic ?? false;\n const finalName = resolveTopicName(topicName, fifo);\n if (fifo && finalName !== topicName) {\n logger.info(\n `setup: appended .fifo suffix to FIFO topic name '${topicName}' -> '${finalName}'`,\n );\n }\n\n // Topic-existence probe via candidate ARN derived from STS account ID.\n const options: ClientOptions = {\n region,\n config: settings.config,\n credentials: parseCredentials(config.credentials),\n timeout: config.timeout,\n };\n const client = settings.client ?? createSnsClient(env, options);\n // A client setup built is closed on every path, unless something in it is\n // the user's (their client, or their request handler instance).\n const closesClient =\n !settings.client && !isHandlerInstance(settings.config?.requestHandler);\n\n try {\n const { accountId, partition } = await getIdentity(env, options, {\n service: 'SNS',\n resource: finalName,\n region,\n id: ctx.id,\n });\n const candidateArn = `arn:${partition}:sns:${region}:${accountId}:${finalName}`;\n\n let topicCreated = false;\n try {\n const Command =\n env?.AWS?.GetTopicAttributesCommand ?? GetTopicAttributesCommand;\n await client.send(new Command({ TopicArn: candidateArn }));\n } catch (err) {\n if (isNotFound(err)) {\n topicCreated = true;\n } else {\n throw err;\n }\n }\n\n // Authoritative-apply: full declared state in one CreateTopic call.\n // SNS:CreateTopic is idempotent on identical Name+Attributes+Tags inputs.\n const attributes: Record<string, string> = {};\n if (fifo) {\n attributes.FifoTopic = 'true';\n attributes.ContentBasedDeduplication = 'true';\n }\n if (merged.displayName !== undefined)\n attributes.DisplayName = merged.displayName;\n if (merged.kmsMasterKeyId !== undefined)\n attributes.KmsMasterKeyId = merged.kmsMasterKeyId;\n\n const tagEntries = merged.tags\n ? Object.entries(merged.tags).map(([Key, Value]) => ({ Key, Value }))\n : undefined;\n\n const createInput: {\n Name: string;\n Attributes?: Record<string, string>;\n Tags?: Array<{ Key: string; Value: string }>;\n } = { Name: finalName };\n if (Object.keys(attributes).length > 0) createInput.Attributes = attributes;\n if (tagEntries && tagEntries.length > 0) createInput.Tags = tagEntries;\n\n const CreateCommand = env?.AWS?.CreateTopicCommand ?? CreateTopicCommand;\n const createRes = await client.send(new CreateCommand(createInput));\n const topicArn = extractTopicArn(createRes);\n\n if (topicCreated) {\n logger.info('setup: topic created', { topicArn, region });\n } else {\n logger.debug('setup: topic exists; declared state re-applied', {\n topicArn,\n region,\n });\n }\n\n const subscriptionsCreated = await applyDeclaredSubscriptions(\n client,\n env,\n topicArn,\n merged.subscriptions ?? [],\n );\n\n return {\n topicArn,\n topicCreated,\n tagsApplied: merged.tags ? Object.keys(merged.tags).length : 0,\n subscriptionsCreated,\n };\n } finally {\n if (closesClient) client.destroy?.();\n }\n}\n","import type { DestinationServer } from '@walkeros/server-core';\nimport type {\n Mapping as WalkerOSMapping,\n Destination as CoreDestination,\n SetupFn as CoreSetupFn,\n} from '@walkeros/core';\nimport type {\n SNSClientConfig,\n CreateTopicCommandInput,\n PublishCommandInput,\n GetTopicAttributesCommandInput,\n SubscribeCommandInput,\n} from '@aws-sdk/client-sns';\nimport type {\n STSClientConfig,\n GetCallerIdentityCommandInput,\n} from '@aws-sdk/client-sts';\nimport type { InFlight, SendClient } from '../../lib/client';\nimport type { CredentialsInput } from '../../lib/credentials';\n\nexport type { SendClient } from '../../lib/client';\n\n/** An AWS SDK v3 command class, as far as this destination builds one. */\nexport type CommandConstructor<Input> = new (input: Input) => {\n readonly input: Input;\n};\n\n/**\n * Settings as a user writes them. `topicArn` alone is enough: region and\n * topic name derive from it. `topicName` (with `region`) also works, and the\n * ARN is then completed with the account id at the first publish.\n */\nexport interface InitSettings {\n /** Topic ARN. Region and topic name derive from it. */\n topicArn?: string;\n /** Topic name (with `.fifo` for FIFO topics). Needed by `walkeros setup`. */\n topicName?: string;\n /** AWS region. Default: the ARN's region, `AWS_REGION` or the profile, else `eu-central-1`. */\n region?: string;\n /** Raw `SNSClient` options, passed through to the SDK. */\n config?: SNSClientConfig;\n /** A client of your own. Used as is and never closed by the destination. */\n client?: SendClient;\n /** Runtime-only, set by `init`: what the destination owns. Not user-facing. */\n runtime?: Runtime;\n}\n\n/** What `init` resolved. */\nexport interface Settings {\n topicArn?: string;\n topicName?: string;\n region: string;\n config?: SNSClientConfig;\n client: SendClient;\n runtime: Runtime;\n}\n\n/** Runtime-only state of one initialized instance. */\nexport interface Runtime {\n /**\n * True when the destination built the client from nothing the user owns,\n * and so closes it. A user client, or a user request handler instance in\n * `settings.config`, keeps it false.\n */\n ownsClient: boolean;\n /** Publishes in flight, awaited by `destroy`. */\n inflight: InFlight;\n /** The topic ARN, completed once with the account id when only a name was given. */\n topicArn: () => Promise<string>;\n}\n\nexport interface Mapping {\n /**\n * Per-event message attributes resolved via mapping. Each attribute value\n * resolves to the SDK's expected `{ DataType, StringValue }` shape; operators\n * write either a literal `{ DataType: 'String', StringValue: '$path' }` value\n * or use `{ value: { DataType: 'String', StringValue: '$user.id' } }` style\n * to drive the StringValue from event data.\n */\n messageAttributes?: WalkerOSMapping.Map;\n /**\n * FIFO group ID for FIFO topics. `Mapping.Value` so operators can write\n * `messageGroupId: 'user.id'` (path) instead of hard-coding a literal.\n */\n messageGroupId?: WalkerOSMapping.Value;\n /**\n * FIFO deduplication ID. `Mapping.Value`, same reasoning as\n * `messageGroupId`. Defaults to the event id on FIFO topics.\n */\n messageDeduplicationId?: WalkerOSMapping.Value;\n}\n\n/**\n * Optional SDK overrides. Each one present wins over the SDK the package\n * imports; tests and simulate inject mocks here.\n */\nexport interface Env extends DestinationServer.Env {\n AWS?: {\n SNSClient?: new (config: SNSClientConfig) => SendClient;\n CreateTopicCommand?: CommandConstructor<CreateTopicCommandInput>;\n PublishCommand?: CommandConstructor<PublishCommandInput>;\n GetTopicAttributesCommand?: CommandConstructor<GetTopicAttributesCommandInput>;\n SubscribeCommand?: CommandConstructor<SubscribeCommandInput>;\n STSClient?: new (config: STSClientConfig) => SendClient;\n GetCallerIdentityCommand?: CommandConstructor<GetCallerIdentityCommandInput>;\n };\n}\n\n/**\n * Provisioning options for `walkeros setup destination.<id>` when targeting SNS.\n * Triggered only by the explicit CLI command. Idempotent, never auto-run.\n *\n * `topicName` lives in Settings (one source of truth for setup AND runtime publish).\n */\nexport interface Setup {\n /** AWS region for the topic. Default: the runtime region order. */\n region?: string;\n /** Display name. Optional. */\n displayName?: string;\n /** FIFO topic with content-based deduplication. Default: false. */\n fifoTopic?: boolean;\n /** KMS key for at-rest encryption. Optional. */\n kmsMasterKeyId?: string;\n /** Tags for cost allocation. Optional. */\n tags?: Record<string, string>;\n /** Subscriptions to create or update on the topic. Each is opt-in. */\n subscriptions?: SetupSubscription[];\n}\n\nexport interface SetupSubscription {\n protocol: 'sqs' | 'lambda' | 'https' | 'http' | 'email' | 'sms';\n endpoint: string;\n rawMessageDelivery?: boolean;\n filterPolicy?: Record<string, unknown>;\n deadLetterTargetArn?: string;\n}\n\nexport type Types = CoreDestination.Types<\n Settings,\n Mapping,\n Env,\n InitSettings,\n Setup,\n CredentialsInput\n>;\n\nexport interface Destination extends DestinationServer.Destination<Types> {\n init: DestinationServer.InitFn<Types>;\n}\n\nexport type Config = {\n settings: InitSettings;\n} & DestinationServer.Config<Types>;\n\nexport type InitFn = DestinationServer.InitFn<Types>;\nexport type PushFn = DestinationServer.PushFn<Types>;\nexport type SetupFn = CoreSetupFn<Config, Env>;\n\nexport type PartialConfig = DestinationServer.PartialConfig<Types>;\n\nexport type PushEvents = DestinationServer.PushEvents<Mapping>;\n\nexport type Rule = WalkerOSMapping.Rule<Mapping>;\nexport type Rules = WalkerOSMapping.Rules<Rule>;\n","import type { Destination } from './types';\nimport { getConfig } from './config';\nimport { push } from './push';\nimport { setup } from './setup';\n\n// Types\nexport * as DestinationSNS from './types';\n\nexport const destinationSNS: Destination = {\n type: 'aws-sns',\n\n config: {},\n\n setup,\n\n async init({ config, env, logger, id }) {\n return getConfig(config, env, logger, id);\n },\n\n push,\n\n async destroy({ config, logger }) {\n const runtime = config.settings?.runtime;\n if (!runtime) return;\n\n // Let publishes on the wire land before closing the client under them.\n await runtime.inflight.settle();\n\n if (!runtime.ownsClient) return;\n runtime.ownsClient = false;\n config.settings?.client?.destroy?.();\n logger.debug('SNS client closed');\n },\n};\n\nexport default destinationSNS;\n"],"mappings":"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","names":["types_exports","import_core","clientConfig","import_client_firehose","import_core","import_core","getConfig","resolveSettings","import_client_sns","import_core","push","resolveSettings","import_client_sns","import_core","types_exports","getConfig","push"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isSameType}from"@walkeros/core";import{throwError}from"@walkeros/core";function isAWSEnvironment(env){return Boolean(env&&"object"==typeof env&&"AWS"in env&&env.AWS?.FirehoseClient)}function getConfig(partialConfig={},env){const settings=partialConfig.settings||{};return settings.firehose&&(settings.firehose=function(firehoseConfig,env){const{streamName:streamName,region:region="eu-central-1",config:config={}}=firehoseConfig;streamName||throwError("Firehose: Config custom streamName missing"),config.region||(config.region=region);let client=firehoseConfig.client;return!client&&isAWSEnvironment(env)&&(client=new env.AWS.FirehoseClient(config)),{streamName:streamName,client:client,region:region}}(settings.firehose,env)),{...partialConfig,settings:settings}}var push=async function(event,context){const{firehose:firehose}=context.config.settings||{};firehose&&async function(pushEvents,config,context){const{client:client,streamName:streamName}=config,{env:env,logger:logger}=context;if(!client)return{queue:pushEvents};const records=pushEvents.map(({event:event})=>({Data:Buffer.from(JSON.stringify(event))}));if(logger.debug("Calling AWS Firehose API",{stream:streamName,recordCount:records.length}),isAWSEnvironment(env))await client.send(new env.AWS.PutRecordBatchCommand({DeliveryStreamName:streamName,Records:records}));else{const{PutRecordBatchCommand:PutRecordBatchCommand}=await import("@aws-sdk/client-firehose");await client.send(new PutRecordBatchCommand({DeliveryStreamName:streamName,Records:records}))}logger?.debug("AWS Firehose API response",{ok:!0})}([{event:event}],firehose,context)},types_exports={},destinationFirehose={type:"aws-firehose",config:{},async init({config:partialConfig,env:env,logger:logger}){const config=getConfig(partialConfig,env);return isSameType(config.settings,{})||logger.throw("Config settings invalid"),config},push:async(event,context)=>await push(event,context)};import{throwError as throwError2}from"@walkeros/core";function isAWSEnvironment2(env){if("object"!=typeof env||null===env)return!1;if(!("AWS"in env))return!1;const aws=env.AWS;return"object"==typeof aws&&null!==aws&&"SNSClient"in aws}function getConfig2(partial={},env){const settings=function(partial,env){const{topicName:topicName,topicArn:topicArn,config:config={}}=partial,region=partial.region??"eu-central-1";topicName||throwError2("SNS: Settings topicName missing");const sdkConfig=config.region?config:{...config,region:region};let client=partial.client;!client&&isAWSEnvironment2(env)&&(client=new env.AWS.SNSClient(sdkConfig));const resolved={topicName:topicName,region:region,config:sdkConfig};return client&&(resolved.client=client),topicArn&&(resolved.topicArn=topicArn),resolved}(partial.settings&&"object"==typeof partial.settings?partial.settings:{},env);return{...partial,settings:settings}}import{getMappingValue,isObject}from"@walkeros/core";function isMessageAttribute(value){if(!isObject(value))return!1;return"string"==typeof value.DataType}async function resolveStringMappingValue(event,value,collector){const resolved=await getMappingValue(event,value,{collector:collector});return"string"==typeof resolved&&resolved.length>0?resolved:null!=resolved?String(resolved):void 0}var push2=async function(event,context){const{config:config,env:env,rule:rule,collector:collector,logger:logger,id:id}=context,settings=config.settings,client=settings?.client,topicArn=settings?.topicArn;if(!client)return logger.throw("SNS push: client missing, init() not run");if(!topicArn)return logger.throw("SNS push: topicArn missing, init() not run");if(!isAWSEnvironment2(env))return logger.throw("SNS push: env.AWS not injected");const ruleSettings=(value=rule?.settings,"object"==typeof value&&null!==value?rule.settings:{});var value;let messageGroupId,messageDeduplicationId;void 0!==ruleSettings.messageGroupId&&(messageGroupId=await resolveStringMappingValue(event,ruleSettings.messageGroupId,collector)),void 0!==ruleSettings.messageDeduplicationId&&(messageDeduplicationId=await resolveStringMappingValue(event,ruleSettings.messageDeduplicationId,collector));const messageAttributes=await async function(event,source,collector){if(!source)return;const result={};let touched=!1;for(const[key,value]of Object.entries(source)){const resolved=await getMappingValue(event,value,{collector:collector});isMessageAttribute(resolved)?(result[key]=resolved,touched=!0):"string"==typeof resolved&&resolved.length>0?(result[key]={DataType:"String",StringValue:resolved},touched=!0):null!=resolved&&(result[key]={DataType:"String",StringValue:String(resolved)},touched=!0)}return touched?result:void 0}(event,ruleSettings.messageAttributes,collector),input={TopicArn:topicArn,Message:JSON.stringify(event)};messageGroupId&&(input.MessageGroupId=messageGroupId),messageDeduplicationId&&(input.MessageDeduplicationId=messageDeduplicationId),messageAttributes&&(input.MessageAttributes=messageAttributes),logger.debug("SNS Publish",{topicArn:topicArn,fifo:topicArn.endsWith(".fifo"),event:event.name});try{await client.send(new env.AWS.PublishCommand(input))}catch(err){throw logger.error(`SNS Publish failed for "${topicArn}". Run "walkeros setup destination.${id}" if the topic is missing.`,{topicArn:topicArn,event:event.name,error:err instanceof Error?err.message:String(err)}),err}};import{resolveSetup}from"@walkeros/core";var DEFAULT_SETUP={region:"eu-central-1",fifoTopic:!1},accountIdCache=new Map;function buildSubscribeAttributes(sub){const attrs={};return void 0!==sub.rawMessageDelivery&&(attrs.RawMessageDelivery=String(sub.rawMessageDelivery)),void 0!==sub.filterPolicy&&(attrs.FilterPolicy=JSON.stringify(sub.filterPolicy)),void 0!==sub.deadLetterTargetArn&&(attrs.RedrivePolicy=JSON.stringify({deadLetterTargetArn:sub.deadLetterTargetArn})),Object.keys(attrs).length>0?attrs:void 0}var types_exports2={};var destinationSNS={type:"aws-sns",config:{},setup:async function(ctx){const{config:config,env:env,logger:logger}=ctx,merged=resolveSetup(config.setup,DEFAULT_SETUP);if(!merged)return void logger.debug("setup: skipped (config.setup is false or unset)");const settings=config.settings;if(!settings||!settings.topicName)return void logger.throw("setup: settings.topicName is required. There is no safe default for the SNS topic name.");const region=merged.region??DEFAULT_SETUP.region,fifo=merged.fifoTopic??!1,declaredName=settings.topicName,finalName=function(name,fifo){if(fifo)return name.endsWith(".fifo")?name:`${name}.fifo`;if(name.endsWith(".fifo"))throw new Error(`FIFO suffix '.fifo' on standard topic name '${name}'. Set setup.fifoTopic: true or rename the topic.`);return name}(declaredName,fifo);fifo&&finalName!==declaredName&&logger.info(`setup: appended .fifo suffix to FIFO topic name '${declaredName}' -> '${finalName}'`);const client=function(settings,env,region){if(settings?.client)return settings.client;const config=settings?.config??{},merged=config.region?config:{...config,region:region};return new env.AWS.SNSClient(merged)}(settings,env,region),accountId=await async function(env,region){const cached=accountIdCache.get(region);if(cached)return cached;const stsClient=new env.AWS.STSClient({region:region}),res=await stsClient.send(new env.AWS.GetCallerIdentityCommand({}));if("object"==typeof res&&null!==res&&"Account"in res&&"string"==typeof res.Account){const accountId=res.Account;return accountIdCache.set(region,accountId),accountId}throw new Error("STS GetCallerIdentity returned no Account")}(env,region),candidateArn=`arn:aws:sns:${region}:${accountId}:${finalName}`;let topicCreated=!1;try{await client.send(new env.AWS.GetTopicAttributesCommand({TopicArn:candidateArn}))}catch(err){if(!function(err){return!(!function(err){return"object"==typeof err&&null!==err}(err)||"NotFoundException"!==err.name&&"NotFound"!==err.name&&(!err.$metadata||404!==err.$metadata.httpStatusCode))}(err))throw err;topicCreated=!0}const attributes={};fifo&&(attributes.FifoTopic="true",attributes.ContentBasedDeduplication="true"),void 0!==merged.displayName&&(attributes.DisplayName=merged.displayName),void 0!==merged.kmsMasterKeyId&&(attributes.KmsMasterKeyId=merged.kmsMasterKeyId);const tagEntries=merged.tags?Object.entries(merged.tags).map(([Key,Value])=>({Key:Key,Value:Value})):void 0,createInput={Name:finalName};Object.keys(attributes).length>0&&(createInput.Attributes=attributes),tagEntries&&tagEntries.length>0&&(createInput.Tags=tagEntries);const topicArn=function(res){if("object"==typeof(v=res)&&null!==v&&"string"==typeof res.TopicArn)return res.TopicArn;var v;throw new Error("SNS CreateTopic returned no TopicArn")}(await client.send(new env.AWS.CreateTopicCommand(createInput)));topicCreated?logger.info("setup: topic created",{topicArn:topicArn,region:region}):logger.debug("setup: topic exists; declared state re-applied",{topicArn:topicArn,region:region});const subscriptionsCreated=await async function(client,env,topicArn,declared){let count=0;for(const sub of declared){const input={TopicArn:topicArn,Protocol:sub.protocol,Endpoint:sub.endpoint},attrs=buildSubscribeAttributes(sub);attrs&&(input.Attributes=attrs),await client.send(new env.AWS.SubscribeCommand(input)),count+=1}return count}(client,env,topicArn,merged.subscriptions??[]);return{topicArn:topicArn,topicCreated:topicCreated,tagsApplied:merged.tags?Object.keys(merged.tags).length:0,subscriptionsCreated:subscriptionsCreated}},async init({config:partial,env:env,logger:logger,id:id}){const config=getConfig2(partial,env);if(!config.settings.client)return void logger.throw("SNS init: client missing (env not injected)");if(!isAWSEnvironment2(env))return void logger.throw("SNS init: env.AWS missing");if(config.settings.topicArn)return config;const{client:client,topicName:topicName,region:region}=config.settings;try{const res=await client.send(new env.AWS.CreateTopicCommand({Name:topicName}));if("object"!=typeof(v=res)||null===v||"string"!=typeof res.TopicArn||0===res.TopicArn.length)return void logger.throw(`SNS init: CreateTopic returned no ARN for "${topicName}"`);config.settings.topicArn=res.TopicArn}catch(err){throw logger.error(`SNS init failed for topic "${topicName}" in region ${region??"eu-central-1"}. Either set settings.topicArn directly, run "walkeros setup destination.${id}", or grant sns:CreateTopic to the runtime role.`,{topicName:topicName,region:region,error:err instanceof Error?err.message:String(err)}),err}var v;return config},push:async(event,context)=>await push2(event,context)};export{types_exports as DestinationFirehose,types_exports2 as DestinationSNS,destinationFirehose as default,destinationFirehose,destinationSNS};//# sourceMappingURL=index.mjs.map
|
|
1
|
+
import{FirehoseClient}from"@aws-sdk/client-firehose";import{isObject as isObject2}from"@walkeros/core";import{loadConfig,NODE_REGION_CONFIG_FILE_OPTIONS,NODE_REGION_CONFIG_OPTIONS}from"@smithy/core/config";function isHandlerInstance(handler){return"object"==typeof handler&&null!==handler&&"handle"in handler&&"function"==typeof handler.handle}function resolveTimeout(timeout){return void 0===timeout||!Number.isFinite(timeout)||timeout<=0?1e4:timeout}function requestHandlerOptions(timeout){return{requestTimeout:Math.floor(.25*resolveTimeout(timeout)),throwOnRequestTimeout:!0}}function parseTopicArn(arn){const parts=arn.split(":");if(6!==parts.length)return;const[prefix,partition,service,region,accountId,name]=parts;return"arn"===prefix&&partition&&"sns"===service&®ion&&accountId&&name?{region:region,accountId:accountId,name:name}:void 0}async function loadSdkRegion(){try{const region=await loadConfig(NODE_REGION_CONFIG_OPTIONS,NODE_REGION_CONFIG_FILE_OPTIONS)();return"string"==typeof region&®ion.length>0?region:void 0}catch{return}}function firstString(...candidates){return candidates.find(candidate=>"string"==typeof candidate&&candidate.length>0)}var InFlight=class{pending=new Set;track(promise){this.pending.add(promise);const remove=()=>{this.pending.delete(promise)};return promise.then(remove,remove),promise}get size(){return this.pending.size}async settle(){for(;this.pending.size>0;)await Promise.allSettled([...this.pending])}};import{isObject}from"@walkeros/core";import{isServerError,isThrottlingError,isTransientError}from"@smithy/core/retry";var AwsDeliveryError=class extends Error{code;status;retryable;constructor(message,options){super(message,{cause:options.cause}),this.name="AwsDeliveryError",this.code=options.code,this.status=options.status,this.retryable=options.retryable}};function formatMessage(target,code,text){const hint=function(code,target){switch(code){case"ResourceNotFoundException":return"Firehose"===target.service?"Check settings.streamName and settings.region.":"Check settings.topicArn and settings.region.";case"AccessDeniedException":case"AuthorizationErrorException":return"Firehose"===target.service?"Allow firehose:PutRecordBatch on the stream for these credentials.":"Allow sns:Publish on the topic for these credentials.";case"UnrecognizedClientException":case"InvalidSignatureException":case"ExpiredTokenException":case"CredentialsProviderError":return"Check config.credentials or the AWS credentials in the environment.";case"NotFoundException":return"SNS"===target.service?`Run "walkeros setup destination.${target.id??"<id>"}" or set settings.topicArn.`:void 0;default:return}}(code,target),base=`${function(target){const kind="Firehose"===target.service?"stream":"topic";return`${target.service} ${kind} "${target.resource}" (${target.region})`}(target)}: ${code}: ${text}`;return hint?`${base} ${hint}`:base}function fromSdkError(error,target){if(error instanceof AwsDeliveryError)return error;if(!function(error){return error instanceof Error}(error))return new AwsDeliveryError(formatMessage(target,"UnknownError",String(error)),{code:"UnknownError",retryable:!1,cause:error});const code=function(error){if("Error"!==error.name)return error.name;const code=Reflect.get(error,"code");return"string"==typeof code&&code.length>0?code:error.name}(error),retryable=isThrottlingError(error)||isTransientError(error)||isServerError(error);return new AwsDeliveryError(formatMessage(target,code,error.message),{code:code,status:error.$metadata?.httpStatusCode,retryable:retryable,cause:error})}function recordError(target,code,text,status){return new AwsDeliveryError(formatMessage(target,code,text),{code:code,status:status,retryable:!0})}function localError(target,code,text,cause){return new AwsDeliveryError(formatMessage(target,code,text),{code:code,retryable:"InvalidResponse"===code,cause:cause})}function configError(message){return new AwsDeliveryError(message,{code:"InvalidConfig",retryable:!1})}function nonEmpty(value){return"string"==typeof value&&value.length>0}function parseCredentials(input){if(null==input||""===input)return;let value=input;if("string"==typeof input)try{value=JSON.parse(input)}catch{throw configError('config.credentials is not valid JSON. Expected {"accessKeyId", "secretAccessKey", "sessionToken"?}.')}if(!isObject(value))throw configError("config.credentials must be an object with accessKeyId and secretAccessKey.");const{accessKeyId:accessKeyId,secretAccessKey:secretAccessKey,sessionToken:sessionToken}=value;for(const[field,fieldValue]of[["accessKeyId",accessKeyId],["secretAccessKey",secretAccessKey]])if(!nonEmpty(fieldValue))throw configError(`config.credentials.${field} is missing or empty. Set it from a $secret.`);if(void 0!==sessionToken&&!nonEmpty(sessionToken))throw configError("config.credentials.sessionToken must be a non-empty string when set.");const credentials={accessKeyId:String(accessKeyId),secretAccessKey:String(secretAccessKey)};return void 0!==sessionToken&&(credentials.sessionToken=sessionToken),credentials}var STREAM_NAME_PATTERN=/^[a-zA-Z0-9_.-]{1,64}$/;function sameValue(a,b){if(a===b)return!0;if(Array.isArray(a)&&Array.isArray(b))return a.length===b.length&&a.every((v,i)=>sameValue(v,b[i]));if(!isObject2(a)||!isObject2(b))return!1;if(!isPlain(a)||!isPlain(b))return!1;const keys=Object.keys(a);return keys.length===Object.keys(b).length&&keys.every(key=>key in b&&sameValue(a[key],b[key]))}function isPlain(value){const proto=Object.getPrototypeOf(value);return proto===Object.prototype||null===proto}function pick(key,flat,alias){if(void 0!==flat&&void 0!==alias&&!sameValue(flat,alias))throw configError(`Firehose: settings.${key} and settings.firehose.${key} are both set and differ. Remove settings.firehose.`);return flat??alias}async function getConfig(partialConfig={},env,logger){const input=partialConfig.settings??{},alias=input.firehose??{},streamName=pick("streamName",input.streamName,alias.streamName),sdkConfig=pick("config",input.config,alias.config),userClient=pick("client",input.client,alias.client),regionSetting=pick("region",input.region,alias.region);if(!streamName)throw configError("Firehose: settings.streamName is missing. Set it to the name of your Firehose stream.");if(!STREAM_NAME_PATTERN.test(streamName))throw configError(`Firehose: settings.streamName "${streamName}" is not a valid stream name. Use 1 to 64 letters, digits, "_", "." or "-".`);const region=firstString(regionSetting,"string"==typeof sdkConfig?.region?sdkConfig.region:void 0,await loadSdkRegion())??"eu-central-1",credentials=parseCredentials(partialConfig.credentials),credentialSource=userClient?"client":credentials?"config.credentials":void 0!==sdkConfig?.credentials?"settings.config.credentials":"default chain",client=userClient??function(Client,options){const clientConfig2={...options.config,region:options.region};return options.credentials&&(clientConfig2.credentials=options.credentials),void 0===clientConfig2.requestHandler&&(clientConfig2.requestHandler=requestHandlerOptions(options.timeout)),new Client(clientConfig2)}(env?.AWS?.FirehoseClient??FirehoseClient,{region:region,config:sdkConfig,credentials:credentials,timeout:partialConfig.timeout}),settings={streamName:streamName,region:region,newline:!1!==input.newline,client:client,runtime:{ownsClient:!userClient&&!isHandlerInstance(sdkConfig?.requestHandler),inflight:new InFlight,credentialSource:credentialSource}};return sdkConfig&&(settings.config=sdkConfig),logger.debug("Firehose client ready",{stream:streamName,region:region,credentials:credentialSource}),{...partialConfig,settings:settings}}function resolveSettings(settings){const{streamName:streamName,region:region,client:client,runtime:runtime}=settings??{};if(!(streamName&®ion&&client&&runtime))throw configError("Firehose: the destination was not initialized.");return{streamName:streamName,region:region,newline:!1!==settings?.newline,client:client,runtime:runtime}}import{PutRecordBatchCommand}from"@aws-sdk/client-firehose";import{isObject as isObject3}from"@walkeros/core";function toPayload(event,data){return isObject3(data)?data:event}function planChunks(payloads,newline){const chunks=[],refused=[];let current=[],currentBytes=0;return payloads.forEach((payload,index)=>{let data;try{data=function(payload,newline=!0){const json=JSON.stringify(payload);if("string"!=typeof json)throw new TypeError("The payload does not serialize to JSON.");return Buffer.from(newline?`${json}\n`:json)}(payload,newline)}catch(error){return void refused.push({index:index,code:"InvalidRecord",detail:`Record ${index} does not serialize to JSON: ${error instanceof Error?error.message:String(error)}`,cause:error})}data.byteLength>1e6?refused.push({index:index,code:"RecordTooLarge",detail:`Record ${index} is ${data.byteLength} bytes, over the 1000000 byte limit.`}):((current.length>=500||currentBytes+data.byteLength>29e5)&&(chunks.push(current),current=[],currentBytes=0),current.push({index:index,data:data}),currentBytes+=data.byteLength)}),current.length>0&&chunks.push(current),{chunks:chunks,refused:refused}}function isRecord(value){return"object"==typeof value&&null!==value}function readMetadata(value){if(!isRecord(value))return{};const metadata=value.$metadata;if(!isRecord(metadata))return{};const{httpStatusCode:httpStatusCode,attempts:attempts}=metadata;return{status:"number"==typeof httpStatusCode?httpStatusCode:void 0,attempts:"number"==typeof attempts?attempts:void 0}}async function deliver(items,settings,env,logger){const target={service:"Firehose",resource:settings.streamName,region:settings.region},Command=env?.AWS?.PutRecordBatchCommand??PutRecordBatchCommand,plan=planChunks(items.map(({event:event,data:data})=>toPayload(event,data)),settings.newline),failures=plan.refused.map(({index:index,code:code,detail:detail,cause:cause})=>({index:index,error:localError(target,code,detail,cause)})),results=await Promise.all(plan.chunks.map(async chunk=>{const started=Date.now(),bytes=chunk.reduce((sum,record)=>sum+record.data.byteLength,0),command=new Command({DeliveryStreamName:settings.streamName,Records:chunk.map(({data:data})=>({Data:data}))});let result,attempts;try{const response=function(value){const response=readMetadata(value);return isRecord(value)?("number"==typeof value.FailedPutCount&&(response.failedPutCount=value.FailedPutCount),Array.isArray(value.RequestResponses)&&(response.responses=value.RequestResponses.map(entry=>{if(!isRecord(entry))return{};const{ErrorCode:ErrorCode,ErrorMessage:ErrorMessage}=entry;return{errorCode:"string"==typeof ErrorCode&&ErrorCode.length>0?ErrorCode:void 0,errorMessage:"string"==typeof ErrorMessage?ErrorMessage:void 0}})),response):response}(await settings.runtime.inflight.track(settings.client.send(command)));attempts=response.attempts,result=function(chunk,response,target){const{responses:responses,failedPutCount:failedPutCount,status:status}=response;if(responses&&responses.length===chunk.length){const failures=[];if(responses.forEach((entry,local)=>{entry.errorCode&&failures.push({index:chunk[local].index,error:recordError(target,entry.errorCode,entry.errorMessage??"Record rejected.",status)})}),void 0===failedPutCount||failures.length===failedPutCount)return failures}else if(0===failedPutCount)return[];const error=localError(target,"InvalidResponse",`PutRecordBatch answered without per-record results that match its FailedPutCount for ${chunk.length} records (FailedPutCount ${failedPutCount??"missing"}).`);return chunk.map(({index:index})=>({index:index,error:error}))}(chunk,response,target)}catch(thrown){attempts=readMetadata(thrown).attempts;const error=fromSdkError(thrown,target);result=chunk.map(({index:index})=>({index:index,error:error}))}return logger.debug("Firehose PutRecordBatch",{stream:settings.streamName,records:chunk.length,bytes:bytes,failed:result.length,attempts:attempts,ms:Date.now()-started}),result}));for(const result of results)failures.push(...result);return failures.sort((a,b)=>a.index-b.index)}function toOutcome(total,failures){if(0!==failures.length){if(failures.length>=total)throw function(failures){const first=failures[0].error;if(failures.every(({error:error})=>error===first))return first;const byCode=new Map;for(const{error:error}of failures){const entry=byCode.get(error.code);entry?entry.count+=1:byCode.set(error.code,{count:1,sample:error})}let dominant={count:0,sample:first};for(const entry of byCode.values())entry.count>dominant.count&&(dominant=entry);const counts=[...byCode.entries()].map(([code,{count:count}])=>`${count} ${code}`).join(", ");return new AwsDeliveryError(`${dominant.sample.message} (all ${failures.length} records failed: ${counts})`,{code:dominant.sample.code,status:dominant.sample.status,retryable:failures.every(({error:error})=>error.retryable),cause:dominant.sample})}(failures);return{failed:failures.map(({index:index,error:error})=>({index:index,error:error}))}}}var types_exports={},destinationFirehose={type:"aws-firehose",config:{batch:{size:500,age:1e3}},init:async({config:config,env:env,logger:logger})=>getConfig(config,env,logger),push:async function(event,context){const{config:config,data:data,env:env,logger:logger}=context,settings=resolveSettings(config.settings),[failure]=await deliver([{event:event,data:data}],settings,env,logger);if(failure)throw failure.error},pushBatch:async(batch,context)=>{const{config:config,env:env,logger:logger}=context;if(0===batch.entries.length)return;const settings=resolveSettings(config.settings),failures=await deliver(batch.entries.map(({event:event,data:data})=>({event:event,data:data})),settings,env,logger);return toOutcome(batch.entries.length,failures)},async destroy({config:config,logger:logger}){const runtime=config.settings?.runtime;runtime&&(await runtime.inflight.settle(),runtime.ownsClient&&(runtime.ownsClient=!1,config.settings?.client?.destroy?.(),logger.debug("Firehose client closed")))}};import{SNSClient}from"@aws-sdk/client-sns";import{GetCallerIdentityCommand,STSClient}from"@aws-sdk/client-sts";import{isObject as isObject4}from"@walkeros/core";function createSnsClient(env,options){return new(env?.AWS?.SNSClient??SNSClient)(function(options){const resolved={...options.config,region:options.region};return options.credentials&&(resolved.credentials=options.credentials),void 0===resolved.requestHandler&&(resolved.requestHandler=requestHandlerOptions(options.timeout)),resolved}(options))}function createStsClient(env,options){const Client=env?.AWS?.STSClient??STSClient,config={region:options.region},credentials=options.credentials??options.config?.credentials;credentials&&(config.credentials=credentials);const userHandler=options.config?.requestHandler;return config.requestHandler=void 0===userHandler||isHandlerInstance(userHandler)?requestHandlerOptions(options.timeout):userHandler,config.maxAttempts=1,new Client(config)}async function getCallerIdentity(client,env,target){const Command=env?.AWS?.GetCallerIdentityCommand??GetCallerIdentityCommand;let response;try{response=await client.send(new Command({}))}catch(error){throw fromSdkError(error,target)}if(!isObject4(response)||"string"!=typeof response.Account)throw fromSdkError(new Error("STS GetCallerIdentity returned no Account."),target);const partition=("string"==typeof response.Arn?response.Arn:"").split(":")[1]||"aws";return{accountId:response.Account,partition:partition}}function topicArnResolver(topicArn,topicName,target,stsClient,env){if(topicArn)return async()=>topicArn;let pending;return()=>(pending??=(async()=>{const client=stsClient();try{const{accountId:accountId,partition:partition}=await getCallerIdentity(client,env,target);return`arn:${partition}:sns:${target.region}:${accountId}:${topicName}`}finally{client.destroy?.()}})().catch(error=>{throw pending=void 0,error}),pending)}import{PublishCommand}from"@aws-sdk/client-sns";import{getMappingValue,isObject as isObject5}from"@walkeros/core";function isMessageAttribute(value){return isObject5(value)&&"string"==typeof value.DataType}async function resolveStringMappingValue(event,value,collector){const resolved=await getMappingValue(event,value,{collector:collector});return"string"==typeof resolved&&resolved.length>0?resolved:null!=resolved?String(resolved):void 0}function readAttempts(value){if("object"!=typeof value||null===value||!("$metadata"in value))return;const metadata=value.$metadata;return"object"==typeof metadata&&null!==metadata&&"attempts"in metadata&&"number"==typeof metadata.attempts?metadata.attempts:void 0}import{CreateTopicCommand,GetTopicAttributesCommand,SubscribeCommand}from"@aws-sdk/client-sns";import{resolveSetup}from"@walkeros/core";var DEFAULT_SETUP={fifoTopic:!1},identityCache=new Map;function buildSubscribeAttributes(sub){const attrs={};return void 0!==sub.rawMessageDelivery&&(attrs.RawMessageDelivery=String(sub.rawMessageDelivery)),void 0!==sub.filterPolicy&&(attrs.FilterPolicy=JSON.stringify(sub.filterPolicy)),void 0!==sub.deadLetterTargetArn&&(attrs.RedrivePolicy=JSON.stringify({deadLetterTargetArn:sub.deadLetterTargetArn})),Object.keys(attrs).length>0?attrs:void 0}var types_exports2={},destinationSNS={type:"aws-sns",config:{},setup:async function(ctx){const{config:config,env:env,logger:logger}=ctx,merged=resolveSetup(config.setup,DEFAULT_SETUP);if(!merged)return void logger.debug("setup: skipped (config.setup is false or unset)");const settings=config.settings??{},arn=settings.topicArn?parseTopicArn(settings.topicArn):void 0,topicName=settings.topicName??arn?.name;if(!topicName)return void logger.throw("setup: settings.topicName (or settings.topicArn) is required. There is no safe default for the SNS topic name.");const sdkRegion="string"==typeof settings.config?.region?settings.config.region:void 0,region=firstString(merged.region,settings.region,arn?.region,sdkRegion,await loadSdkRegion())??"eu-central-1",fifo=merged.fifoTopic??!1,finalName=function(name,fifo){if(fifo)return name.endsWith(".fifo")?name:`${name}.fifo`;if(name.endsWith(".fifo"))throw new Error(`FIFO suffix '.fifo' on standard topic name '${name}'. Set setup.fifoTopic: true or rename the topic.`);return name}(topicName,fifo);fifo&&finalName!==topicName&&logger.info(`setup: appended .fifo suffix to FIFO topic name '${topicName}' -> '${finalName}'`);const options={region:region,config:settings.config,credentials:parseCredentials(config.credentials),timeout:config.timeout},client=settings.client??createSnsClient(env,options),closesClient=!settings.client&&!isHandlerInstance(settings.config?.requestHandler);try{const{accountId:accountId,partition:partition}=await async function(env,options,target){const cached=identityCache.get(options.region);if(cached)return cached;const stsClient=createStsClient(env,options);try{const identity=await getCallerIdentity(stsClient,env,target);return identityCache.set(options.region,identity),identity}finally{stsClient.destroy?.()}}(env,options,{service:"SNS",resource:finalName,region:region,id:ctx.id}),candidateArn=`arn:${partition}:sns:${region}:${accountId}:${finalName}`;let topicCreated=!1;try{const Command=env?.AWS?.GetTopicAttributesCommand??GetTopicAttributesCommand;await client.send(new Command({TopicArn:candidateArn}))}catch(err){if(!function(err){return!(!function(err){return"object"==typeof err&&null!==err}(err)||"NotFoundException"!==err.name&&"NotFound"!==err.name&&(!err.$metadata||404!==err.$metadata.httpStatusCode))}(err))throw err;topicCreated=!0}const attributes={};fifo&&(attributes.FifoTopic="true",attributes.ContentBasedDeduplication="true"),void 0!==merged.displayName&&(attributes.DisplayName=merged.displayName),void 0!==merged.kmsMasterKeyId&&(attributes.KmsMasterKeyId=merged.kmsMasterKeyId);const tagEntries=merged.tags?Object.entries(merged.tags).map(([Key,Value])=>({Key:Key,Value:Value})):void 0,createInput={Name:finalName};Object.keys(attributes).length>0&&(createInput.Attributes=attributes),tagEntries&&tagEntries.length>0&&(createInput.Tags=tagEntries);const CreateCommand=env?.AWS?.CreateTopicCommand??CreateTopicCommand,topicArn=function(res){if("object"==typeof(v=res)&&null!==v&&"string"==typeof res.TopicArn)return res.TopicArn;var v;throw new Error("SNS CreateTopic returned no TopicArn")}(await client.send(new CreateCommand(createInput)));topicCreated?logger.info("setup: topic created",{topicArn:topicArn,region:region}):logger.debug("setup: topic exists; declared state re-applied",{topicArn:topicArn,region:region});const subscriptionsCreated=await async function(client,env,topicArn,declared){let count=0;for(const sub of declared){const input={TopicArn:topicArn,Protocol:sub.protocol,Endpoint:sub.endpoint},attrs=buildSubscribeAttributes(sub);attrs&&(input.Attributes=attrs);const Command=env?.AWS?.SubscribeCommand??SubscribeCommand;await client.send(new Command(input)),count+=1}return count}(client,env,topicArn,merged.subscriptions??[]);return{topicArn:topicArn,topicCreated:topicCreated,tagsApplied:merged.tags?Object.keys(merged.tags).length:0,subscriptionsCreated:subscriptionsCreated}}finally{closesClient&&client.destroy?.()}},init:async({config:config,env:env,logger:logger,id:id})=>async function(partialConfig={},env,logger,id){const input=partialConfig.settings??{},{topicArn:topicArn,config:sdkConfig,client:userClient}=input,arn=topicArn?parseTopicArn(topicArn):void 0;if(topicArn&&!arn)throw configError(`SNS: settings.topicArn "${topicArn}" is not an SNS topic ARN (arn:aws:sns:<region>:<account>:<name>).`);if(!arn&&!input.topicName)throw configError("SNS: settings.topicArn is missing. Set it to the ARN of your topic (or set settings.topicName).");if(arn&&input.region&&input.region!==arn.region)throw configError(`SNS: settings.region "${input.region}" differs from the region "${arn.region}" in settings.topicArn. Remove settings.region.`);if(arn&&input.topicName&&input.topicName!==arn.name)throw configError(`SNS: settings.topicName "${input.topicName}" differs from the name "${arn.name}" in settings.topicArn. Remove settings.topicName.`);const sdkRegion="string"==typeof sdkConfig?.region?sdkConfig.region:void 0,region=firstString(input.region,arn?.region,sdkRegion,await loadSdkRegion())??"eu-central-1",topicName=input.topicName??arn?.name;void 0!==partialConfig.batch&&logger.warn("SNS publishes one message per event and does not batch. Remove config.batch from this destination.");const credentials=parseCredentials(partialConfig.credentials),clientOptions={region:region,config:sdkConfig,credentials:credentials,timeout:partialConfig.timeout},settings={region:region,client:userClient??createSnsClient(env,clientOptions),runtime:{ownsClient:!userClient&&!isHandlerInstance(sdkConfig?.requestHandler),inflight:new InFlight,topicArn:topicArnResolver(topicArn,topicName,{service:"SNS",resource:topicName??"",region:region,id:id},()=>createStsClient(env,clientOptions),env)}};return topicArn&&(settings.topicArn=topicArn),topicName&&(settings.topicName=topicName),sdkConfig&&(settings.config=sdkConfig),logger.debug("SNS client ready",{topic:topicArn??topicName,region:region,credentials:userClient?"client":credentials?"config.credentials":void 0!==sdkConfig?.credentials?"settings.config.credentials":"default chain"}),{...partialConfig,settings:settings}}(config,env,logger,id),push:async function(event,context){const{config:config,data:data,env:env,rule:rule,collector:collector,logger:logger,id:id}=context,settings=function(settings){const{region:region,client:client,runtime:runtime}=settings??{};if(!region||!client||!runtime)throw configError("SNS: the destination was not initialized.");return{topicArn:settings?.topicArn,topicName:settings?.topicName,region:region,client:client,runtime:runtime}}(config.settings),{runtime:runtime}=settings,topicArn=await runtime.inflight.track(runtime.topicArn()),target={service:"SNS",resource:topicArn,region:settings.region,id:id},ruleSettings=(value=rule?.settings,"object"==typeof value&&null!==value?rule.settings:{});var value;const messageGroupId=void 0!==ruleSettings.messageGroupId?await resolveStringMappingValue(event,ruleSettings.messageGroupId,collector):void 0,fifo=topicArn.endsWith(".fifo"),messageDeduplicationId=(void 0!==ruleSettings.messageDeduplicationId?await resolveStringMappingValue(event,ruleSettings.messageDeduplicationId,collector):void 0)??(fifo&&event.id?event.id:void 0),messageAttributes=await async function(event,source,collector){if(!source)return;const result={};let touched=!1;for(const[key,value]of Object.entries(source)){const resolved=await getMappingValue(event,value,{collector:collector});isMessageAttribute(resolved)?(result[key]=resolved,touched=!0):"string"==typeof resolved&&resolved.length>0?(result[key]={DataType:"String",StringValue:resolved},touched=!0):null!=resolved&&(result[key]={DataType:"String",StringValue:String(resolved)},touched=!0)}return touched?result:void 0}(event,ruleSettings.messageAttributes,collector);let message;try{message=JSON.stringify(toPayload(event,data))}catch(error){throw localError(target,"InvalidRecord",`The message does not serialize to JSON: ${error instanceof Error?error.message:String(error)}`,error)}const input={TopicArn:topicArn,Message:message};messageGroupId&&(input.MessageGroupId=messageGroupId),messageDeduplicationId&&(input.MessageDeduplicationId=messageDeduplicationId),messageAttributes&&(input.MessageAttributes=messageAttributes);const Command=env?.AWS?.PublishCommand??PublishCommand,started=Date.now();let attempts;try{attempts=readAttempts(await runtime.inflight.track(settings.client.send(new Command(input))))}catch(error){throw attempts=readAttempts(error),fromSdkError(error,target)}finally{logger.debug("SNS Publish",{topicArn:topicArn,fifo:fifo,event:event.name,attempts:attempts,ms:Date.now()-started})}},async destroy({config:config,logger:logger}){const runtime=config.settings?.runtime;runtime&&(await runtime.inflight.settle(),runtime.ownsClient&&(runtime.ownsClient=!1,config.settings?.client?.destroy?.(),logger.debug("SNS client closed")))}};export{types_exports as DestinationFirehose,types_exports2 as DestinationSNS,destinationFirehose as default,destinationFirehose,destinationSNS};//# sourceMappingURL=index.mjs.map
|