Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions codegen/codegen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,19 @@ output_dir = "javascript/src/models"
[[javascript.task]]
template = "templates/javascript/component_type.ts.jinja"
output_dir = "javascript/src/models"
extra_codegen_args = [
"--include-op-id=v1.endpoint.auto-config.update"
]
[[javascript.task]]
template = "templates/javascript/summary.ts.jinja"
output_dir = "javascript/src"
[[javascript.task]]
template = "templates/javascript/api_resource.ts.jinja"
output_dir = "javascript/src/api_internal"
extra_codegen_args = [
"--include-mode=only-specified",
"--include-op-id=v1.endpoint.auto-config.update"
]


[cli]
Expand Down
5 changes: 5 additions & 0 deletions codegen/generated_files.json
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,10 @@
"javascript/src/api/streamingSink.ts",
"javascript/src/api/streamingStream.ts"
],
[
"javascript/src/api_internal/endpoint.ts",
"javascript/src/api_internal/endpointAutoConfig.ts"
],
[
"javascript/src/index.ts"
],
Expand Down Expand Up @@ -1073,6 +1077,7 @@
"javascript/src/models/streamTokenExpireIn.ts",
"javascript/src/models/stripeConfig.ts",
"javascript/src/models/stripeConfigOut.ts",
"javascript/src/models/subscribeIn.ts",
"javascript/src/models/svixConfig.ts",
"javascript/src/models/svixConfigOut.ts",
"javascript/src/models/telnyxConfig.ts",
Expand Down
6 changes: 4 additions & 2 deletions codegen/templates/javascript/summary.ts.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ const REGIONS = [
{ region: "au", url: "https://api.au.svix.com" },
];


export class Svix {
private readonly requestCtx: SvixRequestContext;
protected readonly requestCtx: SvixRequestContext;

public constructor(token: string, options: SvixOptions = {}) {
const regionalUrl = REGIONS.find((x) => x.region === token.split(".")[1])?.url;
Expand Down Expand Up @@ -102,3 +101,6 @@ export class Svix {
return new OperationalWebhookEndpoint(this.requestCtx);
}
}

// Last to avoid circular dependency
export * from "./autoconfig";
40 changes: 40 additions & 0 deletions javascript/src/api_internal/endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// this file is @generated

import {
type EndpointTransformationIn,
EndpointTransformationInSerializer,
} from "../models/endpointTransformationIn";
import { EndpointAutoConfig } from "./endpointAutoConfig";
import { HttpMethod, SvixRequest, type SvixRequestContext } from "../request";

export class Endpoint {
public constructor(private readonly requestCtx: SvixRequestContext) {}

public get auto_config() {
return new EndpointAutoConfig(this.requestCtx);
}

/**
* This operation was renamed to `set-transformation`.
*
* @deprecated
*/
public transformationPartialUpdate(
appId: string,
endpointId: string,
endpointTransformationIn: EndpointTransformationIn
): Promise<void> {
const request = new SvixRequest(
HttpMethod.PATCH,
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/transformation"
);

request.setPathParam("app_id", appId);
request.setPathParam("endpoint_id", endpointId);
request.setBody(
EndpointTransformationInSerializer._toJsonObject(endpointTransformationIn)
);

return request.sendNoResponseBody(this.requestCtx);
}
}
27 changes: 27 additions & 0 deletions javascript/src/api_internal/endpointAutoConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// this file is @generated

import { type EndpointOut, EndpointOutSerializer } from "../models/endpointOut";
import { type SubscribeIn, SubscribeInSerializer } from "../models/subscribeIn";
import { HttpMethod, SvixRequest, type SvixRequestContext } from "../request";

export class EndpointAutoConfig {
public constructor(private readonly requestCtx: SvixRequestContext) {}

/** Update an auto-config endpoint by providing endpoint details. */
public update(
appId: string,
endpointId: string,
subscribeIn: SubscribeIn
): Promise<EndpointOut> {
const request = new SvixRequest(
HttpMethod.PUT,
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/auto-config"
);

request.setPathParam("app_id", appId);
request.setPathParam("endpoint_id", endpointId);
request.setBody(SubscribeInSerializer._toJsonObject(subscribeIn));

return request.send(this.requestCtx, EndpointOutSerializer._fromJsonObject);
}
}
8 changes: 8 additions & 0 deletions javascript/src/api_internal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Svix } from "..";
import type { SvixRequestContext } from "../request";

export class SvixInternal extends Svix {
public getRequestCtx(): SvixRequestContext {
return this.requestCtx;
}
}
51 changes: 51 additions & 0 deletions javascript/src/autoconfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { test } from "node:test";
import { strict as assert } from "node:assert/strict";

import { AutoConfig, AutoConfigError, Webhook } from "./index";

function makeTokenV1(payload: Record<string, string>): string {
const json = JSON.stringify(payload);
return `auto_v1_${Buffer.from(json, "utf8").toString("base64")}`;
}

test("AutoConfig accepts valid auto_v1 token and verify matches Webhook", () => {
const esec = "whsec_Zm9v";
const token = makeTokenV1({
aid: "app_1",
eid: "ep_2",
surl: "https://api.example.test",
esec,
tok: "sk_test_xyz",
});
const ac = new AutoConfig(token, { url: "https://consumer.example/webhook" });

const payload = '{"hello":"world"}';
const id = "msg_test_autoconfig";
const ts = new Date();
const wh = new Webhook(esec);
const sig = wh.sign(id, ts, payload);
const headers = {
"svix-id": id,
"svix-timestamp": Math.floor(ts.getTime() / 1000).toString(),
"svix-signature": sig,
};

ac.verify(payload, headers);
});

test("AutoConfig rejects wrong prefix", () => {
const json = JSON.stringify({
aid: "a",
eid: "e",
surl: "https://x",
esec: "whsec_Zm9v",
tok: "t",
});
const token = `wrong_${Buffer.from(json, "utf8").toString("base64")}`;
assert.throws(() => new AutoConfig(token, { url: "https://x" }), AutoConfigError);
});

test("AutoConfig rejects invalid JSON payload", () => {
const token = `auto_v1_${Buffer.from("not json", "utf8").toString("base64")}`;
assert.throws(() => new AutoConfig(token, { url: "https://x" }), AutoConfigError);
});
115 changes: 115 additions & 0 deletions javascript/src/autoconfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { SvixInternal } from "./api_internal";
import { Endpoint as InternalEndpoint } from "./api_internal/endpoint";
import type { EndpointIn } from "./models/endpointIn";
import type { EndpointOut } from "./models/endpointOut";
import type { SvixRequestContext } from "./request";
import {
Webhook,
type WebhookRequiredHeaders,
type WebhookUnbrandedRequiredHeaders,
} from "./webhook";

const AUTOCONFIG_TOKEN_PREFIX_V1 = "auto_v1_";

interface AutoConfigTokenContentV1 {
aid: string;
eid: string;
surl: string;
esec: string;
tok: string;
}

export class AutoConfigError extends Error {
public constructor(message = "invalid token") {
super(message);
this.name = "AutoConfigError";
}
}

function isAutoConfigTokenContentV1(value: unknown): value is AutoConfigTokenContentV1 {
if (typeof value !== "object" || value === null) {
return false;
}

const { aid, eid, surl, esec, tok } = value as AutoConfigTokenContentV1;
return (
typeof aid === "string" &&
typeof eid === "string" &&
typeof surl === "string" &&
typeof esec === "string" &&
typeof tok === "string"
);
}

function decodeAutoconfigTokenV1(token: string): AutoConfigTokenContentV1 {
if (!token.startsWith(AUTOCONFIG_TOKEN_PREFIX_V1)) {
throw new AutoConfigError();
}
const b64 = token.slice(AUTOCONFIG_TOKEN_PREFIX_V1.length);
let json: string;

try {
json = Buffer.from(b64, "base64").toString("utf8");
} catch {
throw new AutoConfigError();
}

let parsed: unknown;
try {
parsed = JSON.parse(json);
} catch {
throw new AutoConfigError();
}

if (!isAutoConfigTokenContentV1(parsed)) {
throw new AutoConfigError();
}

return parsed;
}

export class AutoConfig {
private readonly appId: string;
private readonly endpointId: string;
private readonly endpointIn: EndpointIn;
private readonly webhook: Webhook;
private readonly requestCtx: SvixRequestContext;

public constructor(token: string, endpoint: EndpointIn) {
const content = decodeAutoconfigTokenV1(token);
let webhook: Webhook;
try {
webhook = new Webhook(content.esec);
} catch {
throw new AutoConfigError();
}

this.appId = content.aid;
this.endpointId = content.eid;
this.endpointIn = endpoint;
this.webhook = webhook;

const svix = new SvixInternal(content.tok, { serverUrl: content.surl });
this.requestCtx = svix.getRequestCtx();
}

public subscribe(): Promise<EndpointOut> {
return new InternalEndpoint(this.requestCtx).auto_config.update(
this.appId,
this.endpointId,
{
endpoint: this.endpointIn,
}
);
}

public verify(
payload: string | Buffer,
headers:
| WebhookRequiredHeaders
| WebhookUnbrandedRequiredHeaders
| Record<string, string>
): unknown {
return this.webhook.verify(payload, headers);
}
}
1 change: 1 addition & 0 deletions javascript/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./autoconfig.test";
import "./mockttp.test";
import "./KitchenSink.test";
import "./webhook.test";
5 changes: 4 additions & 1 deletion javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const REGIONS = [
];

export class Svix {
private readonly requestCtx: SvixRequestContext;
protected readonly requestCtx: SvixRequestContext;

public constructor(token: string, options: SvixOptions = {}) {
const regionalUrl = REGIONS.find((x) => x.region === token.split(".")[1])?.url;
Expand Down Expand Up @@ -163,3 +163,6 @@ export class Svix {
return new OperationalWebhookEndpoint(this.requestCtx);
}
}

// Last to avoid circular dependency
export * from "./autoconfig";
20 changes: 20 additions & 0 deletions javascript/src/models/subscribeIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// this file is @generated
import { type EndpointIn, EndpointInSerializer } from "./endpointIn";

export interface SubscribeIn {
endpoint: EndpointIn;
}

export const SubscribeInSerializer = {
_fromJsonObject(object: any): SubscribeIn {
return {
endpoint: EndpointInSerializer._fromJsonObject(object["endpoint"]),
};
},

_toJsonObject(self: SubscribeIn): any {
return {
endpoint: EndpointInSerializer._toJsonObject(self.endpoint),
};
},
};
Loading