@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)— PrismafindMany用 WHERE 句
各テナント企業の MCP は本パッケージを npm install するだけで、ミコトの ainotes / password-manager と同じ共有 UX を実装できます。
インストール
npm install @bluelamp/share-sdk @prisma/clientNode.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
レコードに対するアクセス可否を判定します。
判定順序:
user.rankがadminRanks(default['admin'])のいずれかと一致 →truerecord[ownerProperty]がuser.idと一致 →truerecord[ownerProperty]がuser.emailと一致 →true(owner が email で保存されているレガシーケース)record[sharedProperty]配列にuser.emailが含まれる(case-insensitive, trim あり)→true- それ以外 →
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