@bluelamp/share-sdk

v0.1.0

Published

BlueLamp テナント MCP 向けレコード共有 SDK: shared_with TEXT[] パターンで個別レコードを email 単位で共有する API

Readme

@bluelamp/share-sdk

BlueLamp テナント MCP 向けの レコード共有 SDK。

「MCP が扱う個別レコード(メモ 1 件 / シークレット 1 件 / 経費 1 件など)を、BlueLamp ユーザーのメールアドレスで共有する」処理を 4 つの関数に標準化します。

  • canAccess(record, user) — 個別アクセス可否判定
  • addShare(prisma, table, id, email) — 共有先追加(冪等)
  • removeShare(prisma, table, id, email) — 共有先削除(冪等)
  • replaceShares(prisma, table, id, emails) — 共有先一括置換
  • buildShareWhereClause(user) — Prisma findMany 用 WHERE 句

各テナント企業の MCP は本パッケージを npm install するだけで、ミコトの ainotes / password-manager と同じ共有 UX を実装できます。

インストール

npm install @bluelamp/share-sdk @prisma/client

Node.js 18+ が必要です。

クイックスタート

import express from 'express';
import { bluelampAuth } from '@bluelamp/auth-sdk';
import {
  canAccess,
  addShare,
  removeShare,
  buildShareWhereClause,
} from '@bluelamp/share-sdk';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const app = express();
app.use(express.json());

app.use('/api/attendance', bluelampAuth({
  jwksUri: 'https://motohi-api.example/.well-known/jwks.json',
  audience: 'https://x-corp-mcp.example',
  requiredScope: 'attendance:read',
}));

// 取得(自分 or 共有された人のみ閲覧可)
app.get('/api/attendance/:id', async (req, res) => {
  const record = await prisma.attendance.findUnique({ where: { id: req.params.id } });
  if (!record || !canAccess(record, req.user!)) return res.status(403).end();
  res.json(record);
});

// 一覧(自分が見れるものだけ)
app.get('/api/attendance', async (req, res) => {
  const list = await prisma.attendance.findMany({
    where: buildShareWhereClause(req.user!),
    orderBy: { date: 'desc' },
  });
  res.json(list);
});

// 共有追加(owner のみ)
app.post('/api/attendance/:id/share', async (req, res) => {
  const record = await prisma.attendance.findUnique({ where: { id: req.params.id } });
  if (record?.createdBy !== req.user!.id) return res.status(403).end();
  res.json(await addShare(prisma, 'attendance', req.params.id, req.body.email));
});

// 共有削除(owner のみ)
app.delete('/api/attendance/:id/share/:email', async (req, res) => {
  const record = await prisma.attendance.findUnique({ where: { id: req.params.id } });
  if (record?.createdBy !== req.user!.id) return res.status(403).end();
  res.json(await removeShare(prisma, 'attendance', req.params.id, req.params.email));
});

テーブル設計

新規テーブルは以下の規約を満たしてください:

CREATE TABLE your_record (
  id          UUID PRIMARY KEY,
  created_by  UUID NOT NULL,             -- owner ユーザー ID
  shared_with TEXT[] DEFAULT '{}',       -- 共有先 email 配列
  -- 業務カラム --
  ...
);
CREATE INDEX idx_your_record_shared_with ON your_record USING GIN (shared_with);

既存テーブルがすでに allowedOperators / owner_id 等の別名を使っている場合は、SDK の columnName / ownerProperty / sharedProperty / ownerColumn / sharedColumn オプションで差し替えられます。後方互換のためにマイグレーションは不要です。

// 例: password-manager の vault_secrets.allowedOperators をそのまま使う
canAccess(record, user, { ownerProperty: 'createdBy', sharedProperty: 'allowedOperators' });
addShare(prisma, 'vaultSecret', id, email, { columnName: 'allowedOperators' });
buildShareWhereClause(user, { sharedColumn: 'allowedOperators' });

API リファレンス

canAccess(record, user, options?): boolean

レコードに対するアクセス可否を判定します。

判定順序:

  1. user.rank が adminRanks(default ['admin'])のいずれかと一致 → true
  2. record[ownerProperty] が user.id と一致 → true
  3. record[ownerProperty] が user.email と一致 → true(owner が email で保存されているレガシーケース)
  4. record[sharedProperty] 配列に user.email が含まれる(case-insensitive, trim あり)→ true
  5. それ以外 → false

オプション:

  • ownerProperty (default 'createdBy')
  • sharedProperty (default 'shared_with')
  • adminRanks (default ['admin'])

addShare(prisma, tableName, recordId, email, options?): Promise<{ added, shared_with }>

共有先 email を追加(冪等: 既に共有済みなら added: false)。

オプション:

  • columnName (default 'shared_with')
  • idColumnName (default 'id')

removeShare(prisma, tableName, recordId, email, options?): Promise<{ removed, shared_with }>

共有先 email を削除(冪等: 存在しなければ removed: false)。

replaceShares(prisma, tableName, recordId, emails, options?): Promise<{ shared_with }>

共有先 email 配列をまるごと置換。重複と空文字は自動除外(case-insensitive dedup)。

buildShareWhereClause(user, options?): { OR: [...] }

Prisma の where 句に追加するフィルタを返します。

// 戻り値の例
{ OR: [
  { createdBy: 'u-1' },
  { createdBy: '[email protected]' },
  { shared_with: { has: '[email protected]' } },
] }

オプション:

  • ownerColumn (default 'createdBy')
  • sharedColumn (default 'shared_with')

他の条件と組み合わせるときは AND で包んでください:

const where = { AND: [{ category: 'memo' }, buildShareWhereClause(user)] };

同時更新(race condition)

addShare / removeShare は内部で「読む → 配列を書き換える → 書き戻す」を行います。 完全な atomic ではない 点に注意してください:

  • 2 ユーザーが同時に異なる email を追加すると、片方が上書きされる可能性があります
  • ただし同じ email を二重追加して壊れることはありません(読み取り側で has 判定するため)

厳密な atomic が必要な MCP は、SDK の代わりに raw SQL で array_append() + WHERE NOT shared_with @> ARRAY[$1] を使ってください。

ライセンス

MIT

関連

  • 規約 + 詳細ガイド: docs/share-sdk-guide.md(bluelampportal monorepo)
  • 認証 SDK: @bluelamp/auth-sdk