fix(api): sign invites with stored owner keypair instead of unsigned placeholder
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
Production /join on the broker (from feat 18c) rejects every invite
with invite_bad_signature because the web UI was emitting unsigned
payloads. This fixes that.
createMyMesh now generates ed25519 owner keypair + 32-byte root key
and stores all three on the mesh row. createMyInvite loads them,
signs the canonical invite bytes via crypto_sign_detached, and
emits a fully-signed payload matching what the broker expects:
payload = {v, mesh_id, mesh_slug, broker_url, expires_at,
mesh_root_key, role, owner_pubkey, signature}
canonical = same fields minus signature, "|"-delimited
signature = ed25519_sign(canonical, mesh.owner_secret_key)
token = base64url(JSON(payload)) ← stored as invite.token
The base64url(JSON) token IS the DB lookup key — broker's /join
does `WHERE invite.token = <that string>`, then re-verifies the
signature it extracts from the decoded payload.
Also drops the sha256 derivePlaceholderRootKey() helper and the
encodeInviteLink helper, both replaced by inline logic.
backfill extended: the one-off script now populates owner_pubkey
AND owner_secret_key AND root_key together in a single pass. Query
condition is `WHERE any of the three IS NULL`, so running it
post-migration catches every row regardless of partial prior fills.
requires packages/api to depend on libsodium-wrappers + types
(added). 64/64 broker tests still green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,22 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
/**
|
/**
|
||||||
* One-off backfill: populate `mesh.mesh.owner_pubkey` for meshes
|
* One-off backfill: populate owner_pubkey + owner_secret_key +
|
||||||
* created before Step 18c landed.
|
* root_key for meshes created before Step 18c crypto landed.
|
||||||
*
|
*
|
||||||
* Runs idempotently: only touches rows where owner_pubkey IS NULL.
|
* Runs idempotently: only touches rows where ANY of those three
|
||||||
* Generates a fresh ed25519 keypair per mesh and writes the owner
|
* columns is NULL. Generates a fresh keypair + root key per mesh
|
||||||
* SECRET KEY to stdout (paired with mesh_id) so an operator can
|
* and stores ALL THREE server-side (invites are signed server-side
|
||||||
* hand it back to the mesh owner out-of-band.
|
* by the web UI's create-invite flow, so it needs the secret key).
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* DATABASE_URL=... bun apps/broker/scripts/backfill-owner-pubkey.ts
|
* DATABASE_URL=... bun apps/broker/scripts/backfill-owner-pubkey.ts
|
||||||
*
|
*
|
||||||
* Output format (per row): `<mesh_id> <mesh_slug> <owner_pubkey> <owner_secret_key>`
|
* Output (stdout): one tab-separated row per patched mesh:
|
||||||
* Redirect stdout to a secure file — the secret keys grant admin
|
* <mesh_id> <mesh_slug> <owner_pubkey> <owner_secret_key> <root_key>
|
||||||
* invite-signing power and must be stored carefully.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sodium from "libsodium-wrappers";
|
import sodium from "libsodium-wrappers";
|
||||||
import { eq, isNull } from "drizzle-orm";
|
import { eq, isNull, or } from "drizzle-orm";
|
||||||
import { db } from "../src/db";
|
import { db } from "../src/db";
|
||||||
import { mesh } from "@turbostarter/db/schema/mesh";
|
import { mesh } from "@turbostarter/db/schema/mesh";
|
||||||
|
|
||||||
@@ -25,9 +24,21 @@ async function main(): Promise<void> {
|
|||||||
await sodium.ready;
|
await sodium.ready;
|
||||||
|
|
||||||
const missing = await db
|
const missing = await db
|
||||||
.select({ id: mesh.id, slug: mesh.slug, name: mesh.name })
|
.select({
|
||||||
|
id: mesh.id,
|
||||||
|
slug: mesh.slug,
|
||||||
|
ownerPubkey: mesh.ownerPubkey,
|
||||||
|
ownerSecretKey: mesh.ownerSecretKey,
|
||||||
|
rootKey: mesh.rootKey,
|
||||||
|
})
|
||||||
.from(mesh)
|
.from(mesh)
|
||||||
.where(isNull(mesh.ownerPubkey));
|
.where(
|
||||||
|
or(
|
||||||
|
isNull(mesh.ownerPubkey),
|
||||||
|
isNull(mesh.ownerSecretKey),
|
||||||
|
isNull(mesh.rootKey),
|
||||||
|
)!,
|
||||||
|
);
|
||||||
|
|
||||||
if (missing.length === 0) {
|
if (missing.length === 0) {
|
||||||
console.error("[backfill] no rows to patch");
|
console.error("[backfill] no rows to patch");
|
||||||
@@ -39,19 +50,24 @@ async function main(): Promise<void> {
|
|||||||
const kp = sodium.crypto_sign_keypair();
|
const kp = sodium.crypto_sign_keypair();
|
||||||
const pubHex = sodium.to_hex(kp.publicKey);
|
const pubHex = sodium.to_hex(kp.publicKey);
|
||||||
const secHex = sodium.to_hex(kp.privateKey);
|
const secHex = sodium.to_hex(kp.privateKey);
|
||||||
|
const rootKey = sodium.to_base64(
|
||||||
|
sodium.randombytes_buf(32),
|
||||||
|
sodium.base64_variants.URLSAFE_NO_PADDING,
|
||||||
|
);
|
||||||
await db
|
await db
|
||||||
.update(mesh)
|
.update(mesh)
|
||||||
.set({ ownerPubkey: pubHex })
|
.set({
|
||||||
|
ownerPubkey: pubHex,
|
||||||
|
ownerSecretKey: secHex,
|
||||||
|
rootKey,
|
||||||
|
})
|
||||||
.where(eq(mesh.id, row.id));
|
.where(eq(mesh.id, row.id));
|
||||||
// stdout: machine-readable, one mesh per line
|
console.log(
|
||||||
console.log(`${row.id}\t${row.slug}\t${pubHex}\t${secHex}`);
|
`${row.id}\t${row.slug}\t${pubHex}\t${secHex}\t${rootKey}`,
|
||||||
console.error(
|
|
||||||
`[backfill] patched mesh "${row.slug}" (${row.id}) — save its secret key`,
|
|
||||||
);
|
);
|
||||||
|
console.error(`[backfill] patched mesh "${row.slug}" (${row.id})`);
|
||||||
}
|
}
|
||||||
console.error(
|
console.error("[backfill] done.");
|
||||||
"[backfill] done. SECURELY HAND OFF secret keys to mesh owners.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"envin": "catalog:",
|
"envin": "catalog:",
|
||||||
"hono": "4.10.4",
|
"hono": "4.10.4",
|
||||||
|
"libsodium-wrappers": "0.7.15",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"@turbostarter/prettier-config": "workspace:*",
|
"@turbostarter/prettier-config": "workspace:*",
|
||||||
"@turbostarter/tsconfig": "workspace:*",
|
"@turbostarter/tsconfig": "workspace:*",
|
||||||
"@turbostarter/vitest-config": "workspace:*",
|
"@turbostarter/vitest-config": "workspace:*",
|
||||||
|
"@types/libsodium-wrappers": "0.7.14",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { randomBytes, createHash } from "node:crypto";
|
import sodium from "libsodium-wrappers";
|
||||||
|
|
||||||
import { and, eq, isNull } from "@turbostarter/db";
|
import { and, eq, isNull } from "@turbostarter/db";
|
||||||
import { invite, mesh, meshMember } from "@turbostarter/db/schema";
|
import { invite, mesh, meshMember } from "@turbostarter/db/schema";
|
||||||
@@ -11,6 +11,32 @@ import type {
|
|||||||
|
|
||||||
const BROKER_URL = process.env.NEXT_PUBLIC_BROKER_URL ?? "ws://localhost:7900";
|
const BROKER_URL = process.env.NEXT_PUBLIC_BROKER_URL ?? "ws://localhost:7900";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical invite bytes — MUST match the broker's canonicalInvite()
|
||||||
|
* in apps/broker/src/crypto.ts exactly. Any delimiter/field change
|
||||||
|
* between signer and verifier produces `invite_bad_signature`.
|
||||||
|
*/
|
||||||
|
const canonicalInvite = (p: {
|
||||||
|
v: number;
|
||||||
|
mesh_id: string;
|
||||||
|
mesh_slug: string;
|
||||||
|
broker_url: string;
|
||||||
|
expires_at: number;
|
||||||
|
mesh_root_key: string;
|
||||||
|
role: "admin" | "member";
|
||||||
|
owner_pubkey: string;
|
||||||
|
}): string =>
|
||||||
|
`${p.v}|${p.mesh_id}|${p.mesh_slug}|${p.broker_url}|${p.expires_at}|${p.mesh_root_key}|${p.role}|${p.owner_pubkey}`;
|
||||||
|
|
||||||
|
let sodiumReady = false;
|
||||||
|
const ensureSodium = async (): Promise<typeof sodium> => {
|
||||||
|
if (!sodiumReady) {
|
||||||
|
await sodium.ready;
|
||||||
|
sodiumReady = true;
|
||||||
|
}
|
||||||
|
return sodium;
|
||||||
|
};
|
||||||
|
|
||||||
export const createMyMesh = async ({
|
export const createMyMesh = async ({
|
||||||
userId,
|
userId,
|
||||||
input,
|
input,
|
||||||
@@ -29,6 +55,18 @@ export const createMyMesh = async ({
|
|||||||
throw new Error("A mesh with that slug already exists.");
|
throw new Error("A mesh with that slug already exists.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate the mesh owner's ed25519 keypair (signs invites) and a
|
||||||
|
// 32-byte shared root key (channel encryption in later steps).
|
||||||
|
// See mesh.ownerSecretKey comment re: plaintext-at-rest trade-off.
|
||||||
|
const s = await ensureSodium();
|
||||||
|
const kp = s.crypto_sign_keypair();
|
||||||
|
const ownerPubkey = s.to_hex(kp.publicKey);
|
||||||
|
const ownerSecretKey = s.to_hex(kp.privateKey);
|
||||||
|
const rootKey = s.to_base64(
|
||||||
|
s.randombytes_buf(32),
|
||||||
|
s.base64_variants.URLSAFE_NO_PADDING,
|
||||||
|
);
|
||||||
|
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(mesh)
|
.insert(mesh)
|
||||||
.values({
|
.values({
|
||||||
@@ -37,6 +75,9 @@ export const createMyMesh = async ({
|
|||||||
visibility: input.visibility,
|
visibility: input.visibility,
|
||||||
transport: input.transport,
|
transport: input.transport,
|
||||||
ownerUserId: userId,
|
ownerUserId: userId,
|
||||||
|
ownerPubkey,
|
||||||
|
ownerSecretKey,
|
||||||
|
rootKey,
|
||||||
})
|
})
|
||||||
.returning({ id: mesh.id, slug: mesh.slug });
|
.returning({ id: mesh.id, slug: mesh.slug });
|
||||||
|
|
||||||
@@ -87,20 +128,6 @@ export const leaveMyMesh = async ({
|
|||||||
return updated;
|
return updated;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Encode an ic://join/<base64url(JSON)> invite link. Format mirrors
|
|
||||||
* apps/cli/src/invite/parse.ts exactly. */
|
|
||||||
const encodeInviteLink = (payload: unknown): string => {
|
|
||||||
const json = JSON.stringify(payload);
|
|
||||||
const encoded = Buffer.from(json, "utf-8").toString("base64url");
|
|
||||||
return `ic://join/${encoded}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Placeholder deterministic root key until mesh_root_key column lands
|
|
||||||
* (Step 18 crypto). Signature verification is Step 18, so an actual
|
|
||||||
* ed25519 pubkey is not yet required — only presence is checked. */
|
|
||||||
const derivePlaceholderRootKey = (meshId: string, meshSlug: string): string =>
|
|
||||||
createHash("sha256").update(`${meshId}:${meshSlug}`).digest("hex");
|
|
||||||
|
|
||||||
export const createMyInvite = async ({
|
export const createMyInvite = async ({
|
||||||
userId,
|
userId,
|
||||||
meshId,
|
meshId,
|
||||||
@@ -110,12 +137,15 @@ export const createMyInvite = async ({
|
|||||||
meshId: string;
|
meshId: string;
|
||||||
input: CreateMyInviteInput;
|
input: CreateMyInviteInput;
|
||||||
}) => {
|
}) => {
|
||||||
// Authz: owner or admin member can invite
|
// Authz: owner or admin member can invite.
|
||||||
const [meshRow] = await db
|
const [meshRow] = await db
|
||||||
.select({
|
.select({
|
||||||
id: mesh.id,
|
id: mesh.id,
|
||||||
slug: mesh.slug,
|
slug: mesh.slug,
|
||||||
ownerUserId: mesh.ownerUserId,
|
ownerUserId: mesh.ownerUserId,
|
||||||
|
ownerPubkey: mesh.ownerPubkey,
|
||||||
|
ownerSecretKey: mesh.ownerSecretKey,
|
||||||
|
rootKey: mesh.rootKey,
|
||||||
})
|
})
|
||||||
.from(mesh)
|
.from(mesh)
|
||||||
.where(eq(mesh.id, meshId))
|
.where(eq(mesh.id, meshId))
|
||||||
@@ -124,6 +154,15 @@ export const createMyInvite = async ({
|
|||||||
if (!meshRow) {
|
if (!meshRow) {
|
||||||
throw new Error("Mesh not found.");
|
throw new Error("Mesh not found.");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!meshRow.ownerPubkey ||
|
||||||
|
!meshRow.ownerSecretKey ||
|
||||||
|
!meshRow.rootKey
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Mesh is missing owner keypair or root key — run backfill script.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isOwner = meshRow.ownerUserId === userId;
|
const isOwner = meshRow.ownerUserId === userId;
|
||||||
if (!isOwner) {
|
if (!isOwner) {
|
||||||
@@ -143,38 +182,59 @@ export const createMyInvite = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = randomBytes(24).toString("base64url");
|
|
||||||
const expiresAt = new Date(
|
const expiresAt = new Date(
|
||||||
Date.now() + input.expiresInDays * 24 * 60 * 60 * 1000,
|
Date.now() + input.expiresInDays * 24 * 60 * 60 * 1000,
|
||||||
);
|
);
|
||||||
|
const expiresAtSec = Math.floor(expiresAt.getTime() / 1000);
|
||||||
|
|
||||||
|
// Build the canonical signed payload. Signature covers every field
|
||||||
|
// except `signature` itself; broker re-verifies identically.
|
||||||
|
const payloadCore = {
|
||||||
|
v: 1 as const,
|
||||||
|
mesh_id: meshRow.id,
|
||||||
|
mesh_slug: meshRow.slug,
|
||||||
|
broker_url: BROKER_URL,
|
||||||
|
expires_at: expiresAtSec,
|
||||||
|
mesh_root_key: meshRow.rootKey,
|
||||||
|
role: input.role,
|
||||||
|
owner_pubkey: meshRow.ownerPubkey,
|
||||||
|
};
|
||||||
|
const canonical = canonicalInvite(payloadCore);
|
||||||
|
const s = await ensureSodium();
|
||||||
|
const signature = s.to_hex(
|
||||||
|
s.crypto_sign_detached(
|
||||||
|
s.from_string(canonical),
|
||||||
|
s.from_hex(meshRow.ownerSecretKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const fullPayload = { ...payloadCore, signature };
|
||||||
|
|
||||||
|
// The base64url(JSON) is BOTH the link payload AND the DB lookup
|
||||||
|
// token — broker's /join resolves invites by this string.
|
||||||
|
const token = Buffer.from(JSON.stringify(fullPayload), "utf-8").toString(
|
||||||
|
"base64url",
|
||||||
|
);
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(invite)
|
.insert(invite)
|
||||||
.values({
|
.values({
|
||||||
meshId,
|
meshId,
|
||||||
token,
|
token,
|
||||||
|
tokenBytes: canonical,
|
||||||
maxUses: input.maxUses,
|
maxUses: input.maxUses,
|
||||||
role: input.role,
|
role: input.role,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
})
|
})
|
||||||
.returning({ id: invite.id, token: invite.token, expiresAt: invite.expiresAt });
|
.returning({
|
||||||
|
id: invite.id,
|
||||||
const payload = {
|
token: invite.token,
|
||||||
v: 1 as const,
|
expiresAt: invite.expiresAt,
|
||||||
mesh_id: meshRow.id,
|
});
|
||||||
mesh_slug: meshRow.slug,
|
|
||||||
broker_url: BROKER_URL,
|
|
||||||
expires_at: Math.floor(expiresAt.getTime() / 1000),
|
|
||||||
mesh_root_key: derivePlaceholderRootKey(meshRow.id, meshRow.slug),
|
|
||||||
role: input.role,
|
|
||||||
// signature: added in Step 18 (ed25519 sign by mesh_root_key)
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: created!.id,
|
id: created!.id,
|
||||||
token: created!.token,
|
token: created!.token,
|
||||||
expiresAt: created!.expiresAt,
|
expiresAt: created!.expiresAt,
|
||||||
inviteLink: encodeInviteLink(payload),
|
inviteLink: `ic://join/${token}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -302,6 +302,9 @@ importers:
|
|||||||
pdfjs-dist:
|
pdfjs-dist:
|
||||||
specifier: 5.4.530
|
specifier: 5.4.530
|
||||||
version: 5.4.530
|
version: 5.4.530
|
||||||
|
qrcode:
|
||||||
|
specifier: 1.5.4
|
||||||
|
version: 1.5.4
|
||||||
react:
|
react:
|
||||||
specifier: catalog:react19
|
specifier: catalog:react19
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
@@ -363,6 +366,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: catalog:node22
|
specifier: catalog:node22
|
||||||
version: 22.16.0
|
version: 22.16.0
|
||||||
|
'@types/qrcode':
|
||||||
|
specifier: 1.5.6
|
||||||
|
version: 1.5.6
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.2.7
|
specifier: 19.2.7
|
||||||
version: 19.2.7
|
version: 19.2.7
|
||||||
@@ -545,6 +551,9 @@ importers:
|
|||||||
hono:
|
hono:
|
||||||
specifier: 4.10.4
|
specifier: 4.10.4
|
||||||
version: 4.10.4
|
version: 4.10.4
|
||||||
|
libsodium-wrappers:
|
||||||
|
specifier: 0.7.15
|
||||||
|
version: 0.7.15
|
||||||
zod:
|
zod:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.1.13
|
version: 4.1.13
|
||||||
@@ -561,6 +570,9 @@ importers:
|
|||||||
'@turbostarter/vitest-config':
|
'@turbostarter/vitest-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../tooling/vitest
|
version: link:../../tooling/vitest
|
||||||
|
'@types/libsodium-wrappers':
|
||||||
|
specifier: 0.7.14
|
||||||
|
version: 0.7.14
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 9.39.0(jiti@2.6.1)
|
version: 9.39.0(jiti@2.6.1)
|
||||||
@@ -7046,6 +7058,9 @@ packages:
|
|||||||
'@types/prismjs@1.26.5':
|
'@types/prismjs@1.26.5':
|
||||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||||
|
|
||||||
|
'@types/qrcode@1.5.6':
|
||||||
|
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
|
||||||
|
|
||||||
'@types/react-dom@19.0.4':
|
'@types/react-dom@19.0.4':
|
||||||
resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
|
resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -8424,6 +8439,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3:
|
||||||
|
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -11371,6 +11389,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
pngjs@5.0.0:
|
||||||
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0:
|
possible-typed-array-names@1.1.0:
|
||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -11654,6 +11676,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}
|
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
qs@6.14.0:
|
qs@6.14.0:
|
||||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@@ -15888,7 +15915,7 @@ snapshots:
|
|||||||
ci-info: 3.9.0
|
ci-info: 3.9.0
|
||||||
compression: 1.8.0
|
compression: 1.8.0
|
||||||
connect: 3.7.0
|
connect: 3.7.0
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
env-editor: 0.4.2
|
env-editor: 0.4.2
|
||||||
expo: 54.0.27(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.17)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
expo: 54.0.27(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.17)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
||||||
expo-server: 1.0.5
|
expo-server: 1.0.5
|
||||||
@@ -15945,7 +15972,7 @@ snapshots:
|
|||||||
'@expo/plist': 0.4.8
|
'@expo/plist': 0.4.8
|
||||||
'@expo/sdk-runtime-versions': 1.0.0
|
'@expo/sdk-runtime-versions': 1.0.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
getenv: 2.0.0
|
getenv: 2.0.0
|
||||||
glob: 13.0.0
|
glob: 13.0.0
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
@@ -15999,7 +16026,7 @@ snapshots:
|
|||||||
'@expo/env@2.0.8':
|
'@expo/env@2.0.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
dotenv: 16.4.7
|
dotenv: 16.4.7
|
||||||
dotenv-expand: 11.0.7
|
dotenv-expand: 11.0.7
|
||||||
getenv: 2.0.0
|
getenv: 2.0.0
|
||||||
@@ -16012,7 +16039,7 @@ snapshots:
|
|||||||
'@expo/spawn-async': 1.7.2
|
'@expo/spawn-async': 1.7.2
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
getenv: 2.0.0
|
getenv: 2.0.0
|
||||||
glob: 13.0.0
|
glob: 13.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -16056,7 +16083,7 @@ snapshots:
|
|||||||
'@expo/spawn-async': 1.7.2
|
'@expo/spawn-async': 1.7.2
|
||||||
browserslist: 4.25.1
|
browserslist: 4.25.1
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
dotenv: 16.4.7
|
dotenv: 16.4.7
|
||||||
dotenv-expand: 11.0.7
|
dotenv-expand: 11.0.7
|
||||||
getenv: 2.0.0
|
getenv: 2.0.0
|
||||||
@@ -16139,7 +16166,7 @@ snapshots:
|
|||||||
'@expo/image-utils': 0.8.8
|
'@expo/image-utils': 0.8.8
|
||||||
'@expo/json-file': 10.0.8
|
'@expo/json-file': 10.0.8
|
||||||
'@react-native/normalize-colors': 0.81.5
|
'@react-native/normalize-colors': 0.81.5
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
expo: 54.0.27(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.17)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
expo: 54.0.27(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.17)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
semver: 7.7.2
|
semver: 7.7.2
|
||||||
@@ -20534,6 +20561,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/prismjs@1.26.5': {}
|
'@types/prismjs@1.26.5': {}
|
||||||
|
|
||||||
|
'@types/qrcode@1.5.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.0.13
|
||||||
|
|
||||||
'@types/react-dom@19.0.4(@types/react@19.2.7)':
|
'@types/react-dom@19.0.4(@types/react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
@@ -21305,7 +21336,7 @@ snapshots:
|
|||||||
babel-plugin-react-native-web: 0.21.1
|
babel-plugin-react-native-web: 0.21.1
|
||||||
babel-plugin-syntax-hermes-parser: 0.29.1
|
babel-plugin-syntax-hermes-parser: 0.29.1
|
||||||
babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.5)
|
babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.5)
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
react-refresh: 0.14.2
|
react-refresh: 0.14.2
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -22060,6 +22091,8 @@ snapshots:
|
|||||||
|
|
||||||
diff@4.0.2: {}
|
diff@4.0.2: {}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3: {}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type: 4.0.0
|
path-type: 4.0.0
|
||||||
@@ -22850,7 +22883,7 @@ snapshots:
|
|||||||
'@react-navigation/native': 7.1.14(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
'@react-navigation/native': 7.1.14(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
||||||
'@react-navigation/native-stack': 7.3.21(@react-navigation/native@7.1.14(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
'@react-navigation/native-stack': 7.3.21(@react-navigation/native@7.1.14(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
expo: 54.0.27(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.17)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
expo: 54.0.27(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.17)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
|
||||||
expo-constants: 18.0.11(expo@54.0.27)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))
|
expo-constants: 18.0.11(expo@54.0.27)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.2.3))
|
||||||
@@ -25815,6 +25848,8 @@ snapshots:
|
|||||||
pngjs@3.4.0:
|
pngjs@3.4.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
pngjs@5.0.0: {}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss-import@15.1.0(postcss@8.5.6):
|
postcss-import@15.1.0(postcss@8.5.6):
|
||||||
@@ -26028,6 +26063,12 @@ snapshots:
|
|||||||
qrcode-terminal@0.11.0:
|
qrcode-terminal@0.11.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs: 1.0.3
|
||||||
|
pngjs: 5.0.0
|
||||||
|
yargs: 15.4.1
|
||||||
|
|
||||||
qs@6.14.0:
|
qs@6.14.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user