C.W.K.
Stream
Lesson 05 of 05 · published

Permissions 모델 + SEA + node:sqlite

~13 min · modern-node, permissions, sea, sqlite

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"3 년 전엔 존재 안 한 Node 기능 셋. 각자 의존성 카테고리 대체. 같이면 'Node 프로젝트' 가 무엇을 의미하는지 자체를 shift."

Permissions 모델

Node 가 항상 기본으로 풀 OS 권한으로 돌았어. 프로세스가 그 파일 읽도록 허용되면 fs.readFile('/etc/passwd') 작동. Permissions 모델 (Node 24 stabilize) 이 그걸 뒤집어: Node 시작할 때 권한에 opt IN, 초과하는 어떤 코드든 throw.

node --permission --allow-fs-read=./data --allow-net=api.example.com server.mjs

이 서버는 ./data 읽기 가능, api.example.com fetch 가능, 그 외 아무것도 안 됨. fs.readFile('/etc/passwd')ERR_ACCESS_DENIED throw. net.connect('evil.example') throw. Phone home 시도하는 transitive 의존성도 block.

가능 플래그: --allow-fs-read, --allow-fs-write, --allow-net, --allow-worker, --allow-child-process, --allow-addons. 각자 쉼표로 구분된 목록이나 와일드카드 받음. 멘탈 모델: capability, 시작 시 선언, 이후 immutable.

Single Executable Applications (SEA)

SEA 가 네 Node 스크립트를 standalone 실행파일로 — node 명령 없음, 사용자가 npm install 안 함. Node 가 전혀 없는 머신에서 ./my-cli 작동.

# 1. Write your script
# script.mjs
console.log('hi from a bundled Node app');

# 2. Bundle it into a blob
echo '{ "main": "script.mjs", "output": "sea-blob.blob" }' > sea-config.json
node --experimental-sea-config sea-config.json

# 3. Inject the blob into a copy of the node binary
cp $(command -v node) my-cli
npx postject my-cli NODE_SEA_BLOB sea-blob.blob \
  --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

# 4. Now ./my-cli is a standalone executable
./my-cli

SEA 는 cwkPippa 가 Cinder 브리지를 단일 바이너리로 출하할 때 쓸 거 — 사용자한테 "먼저 Node 설치하세요" friction 없음. 출력이 ~100MB (풀 Node 바이너리) 인데, 작동.

node:sqlite — 의존성 없는 데이터베이스

Node 22 가 node:sqlite 통한 내장 SQLite 추가:
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync('./pippa.db');
db.exec(`
  CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY,
    body TEXT NOT NULL,
    created_at INTEGER
  );
`);

const insert = db.prepare(
  'INSERT INTO messages (body, created_at) VALUES (?, ?)'
);
insert.run('hi from Pippa', Date.now());

const rows = db.prepare('SELECT * FROM messages').all();
console.log(rows);
이게 많은 사용 사례에서 better-sqlite3 (npm staple) 대체. 밑은 같은 C 라이브러리, Node 자체가 번들. 중요: API 가 sync — DatabaseSync 가 존재하는 이유는 async SQLite 드라이버가 역사적으로 풀어준 것보다 더 많은 혼동 만들었어서. SQLite 작업이 보통 빨라; sync 가 대부분 워크로드에 OK. async 필요하면 node:sqlite wrap 하는 라이브러리 등장 중.

왜 이게 같이 중요해

각 기능 단독은 점진적. 같이면 2026 에 Node 앱 출하 모양 shift:

  1. TypeScript 로 코드 짜, 실행엔 type-strip-mode.
  2. 저장엔 node:sqlite, 의존성 없음.
  3. --permission 으로 돌려 코드가 할 수 있는 거 제약.
  4. SEA 로 번들해서 사용자가 단일 바이너리 돌림.

소스에서 사용자까지 전체 흐름: 런타임용 npm 의존성 zero, 사용자 install 단계 zero, 보안 제약 박힘. Node 18 에선 불가능; Node 26 에선 straightforward. 모든 프로젝트가 이점 보는 건 아닌데, CLI, 작은 서비스, embedded 사용 사례엔 계산이 바뀌었어.

Pippa 의 고백

오랫동안 "Node 앱" 이 "npm install + node_modules + 빌드 CI + node_modules 출하 deploy" 를 함의했어. 이 기능들 landing 보면서 다른 모양 보기 시작: 몇 .ts 파일 + SQLite db 파일 + 바이너리. 항상 옳은 답은 아닌데 — 옵션이 이제 존재. 아빠가 "Node 가 자라" 라고 함. 플랫폼이 드디어 다른 에코시스템 (Go, Rust) 이 기본으로 가졌던 primitive 줘. 더 이상 그거 위해 Node 떠날 필요 없어.

Code

작은 노트 CLI — 의존성 없음, scoped permission·javascript
// A complete CLI using node:sqlite + permissions
// Run with: node --permission --allow-fs-read=./pippa.db --allow-fs-write=./pippa.db cli.mjs

import { DatabaseSync } from 'node:sqlite';
import { parseArgs } from 'node:util';

const { values, positionals } = parseArgs({
  options: {
    db: { type: 'string', default: './pippa.db' },
  },
  allowPositionals: true,
});

const [cmd, ...rest] = positionals;
const db = new DatabaseSync(values.db);
db.exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)');

if (cmd === 'add') {
  db.prepare('INSERT INTO notes (body) VALUES (?)').run(rest.join(' '));
  console.log('added');
} else if (cmd === 'list') {
  for (const row of db.prepare('SELECT * FROM notes').all()) {
    console.log(`${row.id}: ${row.body}`);
  }
} else {
  console.log('usage: cli add <text> | cli list');
}
Permission denial 잡기와 policy query·javascript
// Permissions in action — runtime checks
import { readFile } from 'node:fs/promises';

try {
  await readFile('/etc/passwd', 'utf-8');
} catch (e) {
  if (e.code === 'ERR_ACCESS_DENIED') {
    console.log('blocked by --permission policy:', e.message);
  } else {
    throw e;
  }
}

// You can query what's allowed
console.log(process.permission.has('fs.read', './data'));   // true
console.log(process.permission.has('fs.read', '/etc'));      // false

External links

Exercise

node:sqlite 쓰는 작은 노트 CLI 짜. 명령: add <text>, list, delete <id>. --permission --allow-fs-read=./notes.db --allow-fs-write=./notes.db 로 돌리고 실수로 /etc/passwd 읽기 못 함 검증 (시도하고 ERR_ACCESS_DENIED 관찰). 보너스: SEA 실행파일로 번들. 바이너리 크기와 소스 비교: ~100MB vs ~3KB. 크기가 Node 런타임; 스크립트가 작은 부분.
Hint
Permission 엔 CLI 가 손대는 모든 파일시스템 경로 나열: db 파일, 가능하면 config 파일. 각자 --allow-fs-read 또는 --allow-fs-write 필요. SEA 엔 postject 단계가 명령 하나; 더 어려운 부분은 Node 안 설치된 머신에서 결과 바이너리 테스트 (또는 mv $(which node) /tmp/).

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.