From 7f8cec68a34988479991410b06547023fb3eb9ac Mon Sep 17 00:00:00 2001 From: Nicolas Burtey Date: Sat, 16 Sep 2023 14:18:56 +0100 Subject: [PATCH] feat: poc for boltcard --- Makefile | 3 + apps/boltcard/.gitattributes | 1 + apps/boltcard/aes.spec.ts | 24 ++++++ apps/boltcard/aes.ts | 86 +++++++++++++++++++ apps/boltcard/bats/e2e-test.bats | 29 +++++++ apps/boltcard/bun.lockb | Bin 0 -> 41699 bytes apps/boltcard/callback.ts | 31 +++++++ apps/boltcard/config.ts | 5 ++ apps/boltcard/decoder.spec.ts | 66 +++++++++++++++ apps/boltcard/decoder.ts | 23 ++++++ apps/boltcard/docker-compose.yml | 12 +++ apps/boltcard/index.ts | 33 ++++++++ apps/boltcard/knex.ts | 136 +++++++++++++++++++++++++++++++ apps/boltcard/lnurlw.ts | 105 ++++++++++++++++++++++++ apps/boltcard/new.ts | 118 +++++++++++++++++++++++++++ apps/boltcard/package.json | 18 ++++ apps/boltcard/router.ts | 3 + tsconfig.json | 2 +- 18 files changed, 694 insertions(+), 1 deletion(-) create mode 100644 apps/boltcard/.gitattributes create mode 100644 apps/boltcard/aes.spec.ts create mode 100644 apps/boltcard/aes.ts create mode 100644 apps/boltcard/bats/e2e-test.bats create mode 100755 apps/boltcard/bun.lockb create mode 100644 apps/boltcard/callback.ts create mode 100644 apps/boltcard/config.ts create mode 100644 apps/boltcard/decoder.spec.ts create mode 100644 apps/boltcard/decoder.ts create mode 100644 apps/boltcard/docker-compose.yml create mode 100644 apps/boltcard/index.ts create mode 100644 apps/boltcard/knex.ts create mode 100644 apps/boltcard/lnurlw.ts create mode 100644 apps/boltcard/new.ts create mode 100644 apps/boltcard/package.json create mode 100644 apps/boltcard/router.ts diff --git a/Makefile b/Makefile index fa66201e5e1..a755d0ad160 100644 --- a/Makefile +++ b/Makefile @@ -160,3 +160,6 @@ codegen: gen-test-jwt: yarn gen-test-jwt + +boltcard: + bun run ./apps/boltcard/index.ts diff --git a/apps/boltcard/.gitattributes b/apps/boltcard/.gitattributes new file mode 100644 index 00000000000..81c05ed14e8 --- /dev/null +++ b/apps/boltcard/.gitattributes @@ -0,0 +1 @@ +*.lockb binary diff=lockb diff --git a/apps/boltcard/aes.spec.ts b/apps/boltcard/aes.spec.ts new file mode 100644 index 00000000000..3d500a7631a --- /dev/null +++ b/apps/boltcard/aes.spec.ts @@ -0,0 +1,24 @@ +const aesCmac = require("node-aes-cmac").aesCmac + +describe("aes", () => { + test(`testing signature`, async () => { + const c = Buffer.from("E19CCB1FED8892CE", "hex") + const aes_cmac_key = Buffer.from("b45775776cb224c75bcde7ca3704e933", "hex") + + const sv2 = Buffer.from([ + 60, 195, 0, 1, 0, 128, 4, 153, 108, 106, 146, 105, 128, 3, 0, 0, + ]) + + const options = { returnAsBuffer: true } + const cmac1 = aesCmac(aes_cmac_key, sv2, options) + const cmac2 = aesCmac(cmac1, new Buffer(""), options) + + const halfMac = Buffer.alloc(cmac2.length / 2) + for (let i = 1; i < cmac2.length; i += 2) { + halfMac[i >> 1] = cmac2[i] + } + + console.log({ c, aes_cmac_key, sv2, cmac1, cmac2 }) + expect(Buffer.compare(halfMac, c)).toEqual(0) + }) +}) diff --git a/apps/boltcard/aes.ts b/apps/boltcard/aes.ts new file mode 100644 index 00000000000..a821143e121 --- /dev/null +++ b/apps/boltcard/aes.ts @@ -0,0 +1,86 @@ +import crypto from "crypto" + +// import { AesCmac } from "aes-cmac" +const aesCmac = require("node-aes-cmac").aesCmac + +const aesjs = require("aes-js") + +class DecryptionError extends Error { + constructor(err: Error) { + super(err.message) + } +} + +class UnknownError extends Error {} + +export function aesDecrypt(key: Buffer, data: Buffer): Buffer | DecryptionError { + try { + const aesCtr = new aesjs.ModeOfOperation.cbc(key) + const decryptedBytes = aesCtr.decrypt(data) + return decryptedBytes + } catch (err) { + console.log(err) + if (err instanceof Error) return new DecryptionError(err) + return new UnknownError() + } +} + +export function checkSignature( + uid: Uint8Array, + ctr: Uint8Array, + k2CmacKey: Buffer, + cmac: Buffer, +): boolean { + const sv2 = Buffer.from([ + 0x3c, + 0xc3, + 0x00, + 0x01, + 0x00, + 0x80, + uid[0], + uid[1], + uid[2], + uid[3], + uid[4], + uid[5], + uid[6], + ctr[0], + ctr[1], + ctr[2], + ]) + + console.log({ sv2 }) + + let calculatedCmac + + try { + calculatedCmac = getSunMAC(k2CmacKey, sv2) + } catch (error) { + console.error(error) + throw new Error("issue with cMac") + } + + console.log({ calculatedCmac, cmac }) + + // Compare the result + return Buffer.compare(calculatedCmac, cmac) === 0 +} + +function getSunMAC(key: Buffer, sv2: Buffer): Buffer { + // const aesCmac = new AesCmac(key) + // const cmac = Buffer.from(await aesCmac.calculate(sv2)) + + const options = { returnAsBuffer: true } + const fullCmacComputed = aesCmac(key, sv2, options) + + // const cmac = aesCmac(key, sv2, options) + console.log({ fullCmacComputed }) + + const result = Buffer.alloc(fullCmacComputed.length / 2) + for (let i = 1; i < fullCmacComputed.length; i += 2) { + result[i >> 1] = fullCmacComputed[i] + } + + return result +} diff --git a/apps/boltcard/bats/e2e-test.bats b/apps/boltcard/bats/e2e-test.bats new file mode 100644 index 00000000000..3339729045c --- /dev/null +++ b/apps/boltcard/bats/e2e-test.bats @@ -0,0 +1,29 @@ + +@test "auth: create card" { + RESPONSE=$(curl -s "http://localhost:3000/createboltcard") + CALLBACK_URL=$(echo $RESPONSE | jq -r '.url') + + # Making the follow-up curl request + RESPONSE=$(curl -s "${CALLBACK_URL}") + echo "$RESPONSE" + [[ $(echo $RESPONSE | jq -r '.PROTOCOL_NAME') == "create_bolt_card_response" ]] || exit 1 +} + + +@test "auth: create payment and follow up" { + + P_VALUE="4E2E289D945A66BB13377A728884E867" + C_VALUE="E19CCB1FED8892CE" + + RESPONSE=$(curl -s "http://localhost:3000/ln?p=${P_VALUE}&c=${C_VALUE}") + echo "$RESPONSE" + + CALLBACK_URL=$(echo $RESPONSE | jq -r '.callback') + K1_VALUE=$(echo $RESPONSE | jq -r '.k1') + + echo "CALLBACK_URL: $CALLBACK_URL" + echo "K1_VALUE: $K1_VALUE" + + # Making the follow-up curl request + curl -s "${CALLBACK_URL}?k1=${K1_VALUE}" +} diff --git a/apps/boltcard/bun.lockb b/apps/boltcard/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..1adae7090c853bbbbe8361e671e6f2684a97f086 GIT binary patch literal 41699 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 Ki2T3r-~R=UGFIRK literal 0 HcmV?d00001 diff --git a/apps/boltcard/callback.ts b/apps/boltcard/callback.ts new file mode 100644 index 00000000000..363f187fd74 --- /dev/null +++ b/apps/boltcard/callback.ts @@ -0,0 +1,31 @@ +import express from "express" + +import { boltcardRouter } from "./router" +import { fetchByK1 } from "./knex" + +boltcardRouter.get("/callback", async (req: express.Request, res: express.Response) => { + const k1 = req?.query?.k1 + const pr = req?.query?.pr + + console.log({ k1, pr }) + + if (!k1 || !pr) { + res.status(400).send({ status: "ERROR", reason: "missing k1 or pr" }) + return + } + + if (typeof k1 !== "string" || typeof pr !== "string") { + res.status(400).send({ status: "ERROR", reason: "invalid k1 or pr" }) + return + } + + const payment = await fetchByK1(k1) + console.log(payment) + // fetch user from k1 + // payInvoice(pr) + + res.json({ status: "OK" }) +}) + +const callback = "dummy" +export { callback } diff --git a/apps/boltcard/config.ts b/apps/boltcard/config.ts new file mode 100644 index 00000000000..03eb0d416dc --- /dev/null +++ b/apps/boltcard/config.ts @@ -0,0 +1,5 @@ +export const serverUrl = process.env.SERVER_URL ?? "http://localhost:3000" + +const AES_DECRYPT_KEY = process.env.AES_DECRYPT_KEY ?? "0c3b25d92b38ae443229dd59ad34b85d" + +export const aesDecryptKey = Buffer.from(AES_DECRYPT_KEY, "hex") diff --git a/apps/boltcard/decoder.spec.ts b/apps/boltcard/decoder.spec.ts new file mode 100644 index 00000000000..200e4965863 --- /dev/null +++ b/apps/boltcard/decoder.spec.ts @@ -0,0 +1,66 @@ +import { aesDecrypt, checkSignature } from "./aes" +import { decryptedPToUidCtr } from "./decoder" + +const aesjs = require("aes-js") + +const values = [ + { + p: aesjs.utils.hex.toBytes("4E2E289D945A66BB13377A728884E867"), + c: Buffer.from("E19CCB1FED8892CE", "hex"), + aes_decrypt_key: aesjs.utils.hex.toBytes("0c3b25d92b38ae443229dd59ad34b85d"), + aes_cmac_key: Buffer.from("b45775776cb224c75bcde7ca3704e933", "hex"), + decrypted_uid: "04996c6a926980", + decrypted_ctr: "030000", + decoded_ctr: 3, + }, + // { + // p: aesjs.utils.hex.toBytes("00F48C4F8E386DED06BCDC78FA92E2FE"), + // c: Buffer.from("66B4826EA4C155B4", "hex"), + // aes_decrypt_key: aesjs.utils.hex.toBytes("0c3b25d92b38ae443229dd59ad34b85d"), + // aes_cmac_key: Buffer.from("b45775776cb224c75bcde7ca3704e933", "hex"), + // decrypted_uid: "04996c6a926980", + // decrypted_ctr: "050000", + // decoded_ctr: 5, + // }, + // { + // p: aesjs.utils.hex.toBytes("0DBF3C59B59B0638D60B5842A997D4D1"), + // c: aesjs.utils.hex.toBytes("66B4826EA4C155B4"), + // aes_decrypt_key: aesjs.utils.hex.toBytes("0c3b25d92b38ae443229dd59ad34b85d"), + // aes_cmac_key: aesjs.utils.hex.toBytes("b45775776cb224c75bcde7ca3704e933"), + // decrypted_uid: "04996c6a926980", + // decrypted_ctr: "070000", + // decoded_ctr: 7, + // }, +] + +describe("crypto", () => { + values.forEach( + ({ + p, + c, + aes_decrypt_key, + aes_cmac_key, + decrypted_uid, + decrypted_ctr, + decoded_ctr, + }) => { + test(`testing ${aesjs.utils.hex.fromBytes(p)}`, async () => { + const decryptedP = aesDecrypt(aes_decrypt_key, p) + if (decryptedP instanceof Error) { + throw decryptedP + } + + const { uid, uidRaw, ctr, ctrRawInverseBytes } = decryptedPToUidCtr(decryptedP) + + expect(uid).toEqual(decrypted_uid) + expect(ctr).toEqual(decoded_ctr) + // expect(ctrRawInverseBytes).toEqual(decrypted_ctr) + + console.log({ uidRaw, ctrRawInverseBytes, aes_cmac_key, c }) + + const cmacVerified = checkSignature(uidRaw, ctrRawInverseBytes, aes_cmac_key, c) + expect(cmacVerified).toEqual(true) + }) + }, + ) +}) diff --git a/apps/boltcard/decoder.ts b/apps/boltcard/decoder.ts new file mode 100644 index 00000000000..19c666d222a --- /dev/null +++ b/apps/boltcard/decoder.ts @@ -0,0 +1,23 @@ +const aesjs = require("aes-js") + +export const decryptedPToUidCtr = ( + decryptedP: Uint8Array, +): { uid: string; uidRaw: Uint8Array; ctr: number; ctrRawInverseBytes: Uint8Array } => { + if (decryptedP[0] !== 0xc7) { + throw new Error("data not starting with 0xC7") + } + + const uidRaw = decryptedP.slice(1, 8) + const uid = aesjs.utils.hex.fromBytes(uidRaw) + + const ctrRawInverseBytes = decryptedP.slice(8, 11) + const ctr = + (ctrRawInverseBytes[2] << 16) | (ctrRawInverseBytes[1] << 8) | ctrRawInverseBytes[0] + + return { + uid, + uidRaw, + ctr, + ctrRawInverseBytes, + } +} diff --git a/apps/boltcard/docker-compose.yml b/apps/boltcard/docker-compose.yml new file mode 100644 index 00000000000..0b8b6263ee9 --- /dev/null +++ b/apps/boltcard/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" +services: + boltcard-pg: + image: postgres:14.1 + ports: + - "5436:5432" + expose: + - "5432" + environment: + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=default diff --git a/apps/boltcard/index.ts b/apps/boltcard/index.ts new file mode 100644 index 00000000000..5d975255f86 --- /dev/null +++ b/apps/boltcard/index.ts @@ -0,0 +1,33 @@ +// server.js + +import express from "express" +import bodyParser from "body-parser" + +import { boltcardRouter } from "./router" + +// loading router +import { lnurlw } from "./lnurlw" +import { callback } from "./callback" +import { createboltcard } from "./new" +import { createTable } from "./knex" + +lnurlw +callback +createboltcard + +await createTable() + +const app = express() +const PORT = 3000 + +// Middleware to parse POST requests +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: true })) + +// Use the router +app.use(boltcardRouter) + +// Start the server +app.listen(PORT, () => { + console.log(`Server started on http://localhost:${PORT}`) +}) diff --git a/apps/boltcard/knex.ts b/apps/boltcard/knex.ts new file mode 100644 index 00000000000..4c52ca70064 --- /dev/null +++ b/apps/boltcard/knex.ts @@ -0,0 +1,136 @@ +const options = { + client: "pg", + connection: "postgres://dbuser:secret@localhost:5436/default?sslmode=disable", +} + +const knex = require("knex")(options) + +export async function createTable() { + const hasPaymentTable = await knex.schema.hasTable("Payment") + + if (!hasPaymentTable) { + await knex.schema.createTable("Payment", (table) => { + table.string("k1", 255).notNullable().index() + + // index on card.id? + table.string("cardId").notNullable() + table.boolean("paid").notNullable().defaultTo(false) + table.timestamp("created_at").defaultTo(knex.fn.now()) + }) + + console.log("Payment table created successfully!") + } else { + console.log("Payment table already exists, skipping table creation.") + } + + const hasCardTable = await knex.schema.hasTable("Card") + + if (!hasCardTable) { + await knex.schema.createTable("Card", (table) => { + table.string("id").notNullable().index().unique() + + // if a card is resetted, the uid would stay the same + table.string("uid").notNullable().index() + table.uuid("accountId").notNullable().index() + table.boolean("enabled").notNullable().defaultTo(true) + + table.string("k0AuthKey").notNullable() + table.string("k2CmacKey").notNullable() + table.string("k3").notNullable() + table.string("k4").notNullable() + }) + console.log("Card table created successfully!") + } else { + console.log("Card table already exists, skipping table creation.") + } + + const hasCardInitTable = await knex.schema.hasTable("CardInit") + + if (!hasCardInitTable) { + await knex.schema.createTable("CardInit", (table) => { + table.string("oneTimeCode").notNullable().index().unique() + table.timestamp("created_at").defaultTo(knex.fn.now()) + table.string("status").defaultTo("init") // init, fetched, used + + table.string("k0AuthKey").notNullable() + table.string("k2CmacKey").notNullable() + table.string("k3").notNullable() + table.string("k4").notNullable() + }) + console.log("CardInit table created successfully!") + } else { + console.log("CardInit table already exists, skipping table creation.") + } +} + +export async function insertk1({ k1, cardId }: { k1: string; cardId: string }) { + await knex("Payment").insert({ k1, cardId }) + console.log("k1 inserted successfully!") +} + +export async function fetchByK1(k1: string) { + const result = await knex("Payment").where("k1", k1).first() + return result +} + +export async function fetchByUid(uid: string) { + const result = await knex("Card").where("uid", uid).first() + return result +} + +export async function fetchByCardId(cardId: string) { + const result = await knex("Card").where("id", cardId).first() + return result +} + +interface CardInput { + oneTimeCode: string + k0AuthKey: string + k2CmacKey: string + k3: string + k4: string +} + +export async function createCardInit(cardData: CardInput) { + try { + const { oneTimeCode, k0AuthKey, k2CmacKey, k3, k4 } = cardData + + const result = await knex("CardInit").insert({ + oneTimeCode, + k0AuthKey, + k2CmacKey, + k3, + k4, + }) + + return result + } catch (error) { + if (error instanceof Error) console.error(`Error creating card: ${error.message}`) + throw error + } +} + +export async function fetchByOneTimeCode(oneTimeCode: string) { + const result = await knex("CardInit").where("oneTimeCode", oneTimeCode).first() + + if (result) { + await knex("CardInit").where("oneTimeCode", oneTimeCode).update({ status: "fetched" }) + } + + return result +} + +export async function fetchAllWithStatusFetched() { + const results = await knex("CardInit").where("status", "fetched").select() + return results +} + +// async function main() { +// await createTable() +// await insertk1() +// } + +// main().catch((error) => { +// console.error("Error:", error) +// process.exit(1) +// }) diff --git a/apps/boltcard/lnurlw.ts b/apps/boltcard/lnurlw.ts new file mode 100644 index 00000000000..a9c64cf0d66 --- /dev/null +++ b/apps/boltcard/lnurlw.ts @@ -0,0 +1,105 @@ +import { randomBytes } from "crypto" + +import express from "express" + +import { aesDecrypt } from "./aes" +import { aesDecryptKey, serverUrl } from "./config" +import { decryptedPToUidCtr } from "./decoder" +import { fetchAllWithStatusFetched, fetchByUid, insertk1 } from "./knex" +import { boltcardRouter } from "./router" + +function generateSecureRandomString(length: number): string { + return randomBytes(Math.ceil(length / 2)) + .toString("hex") + .slice(0, length) +} + +const maybeSetupCard = async ({ + uid, + ctr, + ba_c, +}: { + uid: string + ctr: number + ba_c: Buffer +}) => { + const cardInits = await fetchAllWithStatusFetched() + + for (const cardInit of cardInits) { + console.log({ cardInit }, "cardInit") + const k2 = cardInit.k2CmacKey + + console.log(k2) + } +} + +boltcardRouter.get("/ln", async (req: express.Request, res: express.Response) => { + const raw_p = req?.query?.p + const raw_c = req?.query?.c + + if (!raw_p || !raw_c) { + res.status(400).send({ status: "ERROR", reason: "missing p or c" }) + return + } + + if (raw_p?.length !== 32 || raw_c?.length !== 16) { + res.status(400).send({ status: "ERROR", reason: "invalid p or c" }) + return + } + + if (typeof raw_p !== "string" || typeof raw_c !== "string") { + res.status(400).send({ status: "ERROR", reason: "invalid p or c" }) + return + } + + const ba_p = Buffer.from(raw_p, "hex") + const ba_c = Buffer.from(raw_c, "hex") + + console.log({ ba_p, ba_c }) + + const decryptedP = aesDecrypt(aesDecryptKey, ba_p) + if (decryptedP instanceof Error) { + res.status(400).send({ status: "ERROR", reason: "impossible to decrypt P" }) + return + } + + // TODO error management + const { uid, uidRaw, ctr } = decryptedPToUidCtr(decryptedP) + + console.log({ + uid, + uidRaw, + ctr, + }) + + const card = await fetchByUid(uid) + console.log({ card }, "card") + + if (!card) { + maybeSetupCard() + + res.status(400).send({ status: "ERROR", reason: "card not found" }) + return + } + + if (!card.enabled) { + res.status(400).send({ status: "ERROR", reason: "card disabled" }) + return + } + + const k1 = generateSecureRandomString(32) + + await insertk1({ k1, cardId: card.id }) + + res.json({ + tag: "withdrawRequest", + callback: serverUrl + "/callback", + k1, + defaultDescription: "payment for a blink card", + minWithdrawable: 1000, + maxWithdrawable: 100000000000, + }) +}) + +const lnurlw = "dummy" +export { lnurlw } diff --git a/apps/boltcard/new.ts b/apps/boltcard/new.ts new file mode 100644 index 00000000000..6e41fb6ac37 --- /dev/null +++ b/apps/boltcard/new.ts @@ -0,0 +1,118 @@ +import { randomBytes } from "crypto" + +import express from "express" + +import { boltcardRouter } from "./router" +import { aesDecryptKey, serverUrl } from "./config" +import { createCardInit, fetchByOneTimeCode } from "./knex" + +function randomHex(): string { + try { + const bytes: Buffer = randomBytes(16) + return bytes.toString("hex") + } catch (error) { + if (error instanceof Error) { + console.warn(error.message) + throw error + } + } +} + +// curl -s http://localhost:3000/createboltcard + +boltcardRouter.get( + "/createboltcard", + async (req: express.Request, res: express.Response) => { + const oneTimeCode = randomHex() + const k0AuthKey = randomHex() + const k2CmacKey = randomHex() + const k3 = randomHex() + const k4 = randomHex() + + const result = await createCardInit({ + oneTimeCode, + k0AuthKey, + k2CmacKey, + k3, + k4, + }) + + if (result instanceof Error) { + res.status(400).send({ status: "ERROR", reason: "impossible to create card" }) + return + } + + const url = `${serverUrl}/new?a=${oneTimeCode}` + res.json({ + status: "OK", + url, + }) + }, +) + +interface NewCardResponse { + PROTOCOL_NAME: string + PROTOCOL_VERSION: number + CARD_NAME: string + LNURLW_BASE: string + K0: string + K1: string + K2: string + K3: string + K4: string + UID_PRIVACY: string +} + +boltcardRouter.get("/new", async (req: express.Request, res: express.Response) => { + const url = req.url + console.debug("new_card url:", url) + + const oneTimeCode = req.query.a + + if (!oneTimeCode) { + console.debug("a not found") + res.status(400).send({ status: "ERROR", reason: "a missing" }) + return + } + + if (typeof oneTimeCode !== "string") { + console.debug("a is not a string") + res.status(400).send({ status: "ERROR", reason: "Bad Request" }) + return + } + + const cardInit = await fetchByOneTimeCode(oneTimeCode) + + if (!cardInit) { + console.debug("cardInit not found") + res.status(400).send({ status: "ERROR", reason: "cardInit not found" }) + return + } + + if (cardInit.status !== "init") { + console.debug("cardInit already fetched") + res.status(400).send({ status: "ERROR", reason: "code has been fetched" }) + return + } + + const lnurlwBase = `${serverUrl}/ln` + const k1DecryptKey = aesDecryptKey.toString("hex") + + const response: NewCardResponse = { + PROTOCOL_NAME: "create_bolt_card_response", + PROTOCOL_VERSION: 2, + CARD_NAME: "dummy", + LNURLW_BASE: lnurlwBase, + K0: cardInit.k0AuthKey, + K1: k1DecryptKey, + K2: cardInit.k2CmacKey, + K3: cardInit.k3, + K4: cardInit.k4, + UID_PRIVACY: "Y", + } + + res.status(200).json(response) +}) + +const createboltcard = "" +export { createboltcard } diff --git a/apps/boltcard/package.json b/apps/boltcard/package.json new file mode 100644 index 00000000000..7360c90c89d --- /dev/null +++ b/apps/boltcard/package.json @@ -0,0 +1,18 @@ +{ + "name": "boltcard", + "version": "0.0.1", + "main": "index.ts", + "license": "MIT", + "dependencies": { + "aes-cmac": "^2.0.0", + "aes-js": "^3.1.2", + "body-parser": "^1.20.2", + "express": "^4.18.2", + "knex": "^2.5.1", + "node-aes-cmac": "^0.1.1", + "pg": "^8.11.3" + }, + "devDependencies": { + "@types/pg": "^8.10.2" + } +} diff --git a/apps/boltcard/router.ts b/apps/boltcard/router.ts new file mode 100644 index 00000000000..5b173dcc642 --- /dev/null +++ b/apps/boltcard/router.ts @@ -0,0 +1,3 @@ +import express from "express" + +export const boltcardRouter = express.Router() diff --git a/tsconfig.json b/tsconfig.json index 7d9d80f900b..4b5be3bac74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,7 @@ } }, "compileOnSave": true, - "include": ["src/**/*", "test/**/*"], + "include": ["src/**/*", "test/**/*", "apps/**/*"], "watchOptions": { "synchronousWatchDirectory": true }