# 組織とワークスペース

> TypeScript から Nodaro の組織とワークスペースを管理します。学校やチームを作成し、メンバーを招待し、参加コードを発行し、クレジットの使用状況レポートを読み取ります。

Source: https://nodaro.ai/ja/docs/developers/sdk/organizations

**`client.organizations`** は、組織（学校やチーム）を、そのメンバー、招待、監査ログ、使用状況レポートとともに管理します。**`client.workspaces`** は、その中にあるワークスペース（クラスやチームプロジェクトなど）を、そのメンバーと参加コードとともに管理します。これらのメソッドは、[ワークスペースの REST API](https://nodaro.ai/docs/developers/api/workspaces) を呼び出します。概念については、[ワークスペース](https://nodaro.ai/docs/concepts/workspaces)を参照してください。

組織は Nodaro Cloud の機能で、インスタンスごとに有効にします。組織がないインスタンスでは、ここにあるすべてのメソッドが `NotFoundError` をスローします。

## 所属と操作の違い
ワークスペースに所属することと、そこで操作することは、別のことです。この 2 つのリソースは、誰がどこに所属しているかを管理します。ワークスペースで**操作**して、一覧をそこから読み取り、新しい作業をそこに置くには、[`client.withWorkspace(workspaceId)`](https://nodaro.ai/docs/developers/sdk/client#withworkspaceworkspaceid) でクライアントを作成するか、`createClient` に `workspaceId` を渡します。

```ts
const classroom = client.withWorkspace(workspaceId)
await classroom.workflows.run(workflowId) // runs in the class
await client.workflows.run(workflowId)    // runs in your personal space
```

[`client.me()`](https://nodaro.ai/docs/developers/sdk/client#me) は、自分が属する組織とワークスペース、そして `lastWorkspaceId` を返します。

すべての権限は、サーバーが決めます。呼び出し元が招待、削除、名前変更を行えるかどうかはサーバーが判断し、できない場合はエラーとして返します。設定によって変わることがあるため、自分のクライアントでは推測しないでください。

## メソッド
| メソッド | 内容 |
| --- | --- |
| [`organizations.list()`](#organizationslist) | 自分が属する組織を一覧表示します |
| [`organizations.get(id)`](#organizationsgetid) | 1 つの組織を読み取ります |
| [`organizations.create(input)`](#organizationscreateinput) | 学校またはチームを作成します |
| [`organizations.update(id, input)`](#organizationsupdateid-input) | 名前を変更するか、設定を変更します |
| [`organizations.delete(id)`](#organizationsdeleteid) | 組織を削除します |
| [`organizations.transferOwnership(id, userId)`](#organizationstransferownershipid-userid) | 別のメンバーをオーナーにします |
| [`organizations.leave(id)`](#organizationsleaveid) | 組織を脱退します |
| [`organizations.listMembers()`、`updateMember()`、`removeMember()`](#organization-members) | メンバーを管理します |
| [`organizations.invite(orgId, input)`](#organizationsinviteorgid-input) | メールでメンバーを招待します |
| [`organizations.listInvitations()`、`revokeInvitation()`、`resendInvitation()`](#manage-invitations) | 送信済みの招待を管理します |
| [`organizations.previewInvitation()`、`acceptInvitation()`](#preview-and-accept-an-invitation) | 招待を表示し、承認します |
| [`organizations.audit(orgId, opts?)`](#organizationsauditorgid-opts) | 監査ログを読み取ります |
| [`organizations.usage()`、`usageRows()`、`usageCsv()`](#organization-usage) | クレジットの使用状況を読み取ります |
| [`workspaces.list()`](#workspaceslist) | 自分が属するワークスペースを一覧表示します |
| [`workspaces.listForOrg(orgId, opts?)`](#workspaceslistfororgorgid-opts) | 組織のワークスペースを一覧表示します |
| [`workspaces.get(id)`](#workspacesgetid) | 1 つのワークスペースを読み取ります |
| [`workspaces.create(orgId, input)`](#workspacescreateorgid-input) | ワークスペースを作成します |
| [`workspaces.update(id, input)`](#workspacesupdateid-input) | ワークスペースを変更します |
| [`workspaces.setArchived(id, archived)`](#workspacessetarchivedid-archived) | ワークスペースをアーカイブまたはアーカイブ解除します |
| [`workspaces.listMembers()`、`addMember()`、`updateMember()`、`removeMember()`](#workspace-members) | ワークスペースのメンバーを管理します |
| [`workspaces.getJoinCode()`、`actOnJoinCode()`](#join-codes) | 参加コードを管理します |
| [`workspaces.join(code)`](#workspacesjoincode) | コードでワークスペースに参加します |
| [`workspaces.usage()`、`usageRows()`、`usageCsv()`](#workspace-usage) | ワークスペースのクレジットの使用状況を読み取ります |

## client.organizations
### organizations.list()
自分が属する組織を一覧表示します（`GET /v1/orgs`）。

```ts
list(): Promise<{ data: OrganizationView[] }>
```

```ts
const { data: orgs } = await client.organizations.list()
```

### organizations.get(id)
1 つの組織を読み取ります（`GET /v1/orgs/:id`）。

```ts
get(id: string): Promise<{ data: OrganizationView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "組織の ID です。" },
}}
/>

```ts
const { data: org } = await client.organizations.get(orgId)
console.log(org.status) // pending, active, suspended or deleted
```

### organizations.create(input)
組織を作成し、自分がそのオーナーになります（`POST /v1/orgs`）。新しい組織は、通常は `pending` から始まり、Nodaro が承認すると `active` になります。pending の間は、自分だけがそれを見ることができ、中身は何も変更できないため、承認を待っていることをユーザーに伝えてください。

```ts
create(input: { name: string; kind: "school" | "team"; slug?: string; acceptTerms?: boolean; settings?: OrgSettings }): Promise<{ data: OrganizationView }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "組織の名前です。" },
kind: { type: '"school" | "team"', required: true, description: "組織の種類です。デフォルトの設定を決めます。" },
slug: { type: 'string', description: "一意の短い名前です。使用中のスラッグを指定すると、409 name_taken で失敗します。" },
acceptTerms: { type: 'boolean', description: "学校では必須です。オーナーが、生徒を登録する権限があることを確認します。" },
settings: { type: 'OrgSettings', description: "その組織のすべてのワークスペースに適用される設定です。" },
}}
/>

```ts
const { data: school } = await client.organizations.create({
name: "Sunrise School",
kind: "school",
acceptTerms: true,
})
```

`acceptTerms: true` のない学校は、`400 terms_required` で失敗します。

### organizations.update(id, input)
組織の名前を変更するか、設定を変更します（`PATCH /v1/orgs/:id`）。

```ts
update(id: string, input: { name?: string; settings?: OrgSettings }): Promise<{ data: OrganizationView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "組織の ID です。" },
name: { type: 'string', description: "新しい名前です。" },
settings: { type: 'OrgSettings', description: "変更する設定です。下の設定の表を参照してください。" },
}}
/>

```ts
await client.organizations.update(orgId, {
settings: { default_workflow_visibility: "workspace", allowed_email_domains: ["school.example"] },
})
```

設定は階層化されています。ワークスペースの設定は組織の設定より優先され、組織の設定は、その種類のデフォルトより優先されます。キーを `false` に設定することは、実際の値であり、「未設定」ではありません。

| キー | 型 | 意味 |
| --- | --- | --- |
| `admin_access` | `view` または `edit` | 管理者がメンバーのワークフローに対して行える操作です。 |
| `default_workflow_visibility` | `private` または `workspace` | 新しいワークフローの公開範囲です。 |
| `member_access_to_shared` | `view` または `edit` | メンバーが、ワークスペースに共有されたワークフローに対して行える操作です。 |
| `members_can_create_projects` | 真偽値 | メンバーがワークスペースでプロジェクトを作成できるかどうかです。 |
| `member_caps_enabled` | 真偽値 | メンバーごとのクレジット上限を適用するかどうかです。 |
| `personal_space_enabled` | 真偽値 | メンバーが個人スペースを持てるかどうかです。 |
| `workspace_admins_can_invite` | 真偽値 | ワークスペースの管理者が、新しい人を招待できるかどうかです。 |
| `collaborators_can_invite` | 真偽値 | 編集者権限の共同編集者が、さらに共同編集者を招待できるかどうかです。 |
| `allowed_email_domains` | 文字列のリスト | 組織のみ：参加を許可するメールドメインです。 |
| `vocabulary_overrides` | オブジェクト | 組織のみ：`{ "workspace": "Cohort" }` のような、その種類の語句の新しいラベルです。 |

組織のみの 2 つのキーは、更新すると全体が置き換わります。

### organizations.delete(id)
組織を削除します（`DELETE /v1/orgs/:id`）。元のメンバーからは見えなくなりますが、何も破棄されません。先にすべてのワークスペースをアーカイブしてください。そうしないと、呼び出しは `409 has_active_workspaces` で失敗します。

```ts
delete(id: string): Promise<{ data: { id: string; status: string } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "組織の ID です。" },
}}
/>

```ts
await client.organizations.delete(orgId)
```

### organizations.transferOwnership(id, userId)
別のメンバーをオーナーにします（`POST /v1/orgs/:id/transfer-ownership`）。自分は管理者になります。

```ts
transferOwnership(id: string, userId: string): Promise<{ data: { orgId: string; ownerUserId: string } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "組織の ID です。" },
userId: { type: 'string', required: true, description: "オーナーになるメンバーです。" },
}}
/>

```ts
await client.organizations.transferOwnership(orgId, newOwnerId)
```

### organizations.leave(id)
組織を脱退します（`POST /v1/orgs/:id/leave`）。オーナーは脱退できません。先にオーナー権限を譲渡してください。そうしないと、呼び出しは `409 owner_cannot_leave` で失敗します。

```ts
leave(id: string): Promise<{ data: { orgId: string; left: boolean } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "組織の ID です。" },
}}
/>

```ts
await client.organizations.leave(orgId)
```

### 組織のメンバー
組織のメンバーを一覧表示、変更、削除します（`/v1/orgs/:id/members`）。メンバーは `active` または `suspended` です。停止されたメンバーは席を保持しますが、操作はできません。オーナーは、停止、削除、降格のいずれもされません。

```ts
listMembers(orgId: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<OrgMemberView>>
updateMember(orgId: string, userId: string, input: { role?: "admin" | "member"; status?: "active" | "suspended" }): Promise<{ data: OrgMemberView }>
removeMember(orgId: string, userId: string): Promise<{ data: { removed: boolean } }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "組織の ID です。" },
userId: { type: 'string', description: "メンバーです。updateMember と removeMember で必須です。" },
cursor: { type: 'string', description: "listMembers：前のページの nextCursor です。" },
limit: { type: 'number', description: "listMembers：ページのサイズです。" },
role: { type: '"admin" | "member"', description: "updateMember：新しいロールです。" },
status: { type: '"active" | "suspended"', description: "updateMember：メンバーを停止するか、復帰させます。" },
}}
/>

```ts
const { data: members, nextCursor } = await client.organizations.listMembers(orgId)
await client.organizations.updateMember(orgId, userId, { role: "admin" })
```

### organizations.invite(orgId, input)
メールでメンバーを招待します（`POST /v1/orgs/:id/invitations`）。**アドレスごとに 1 行**を返します。

```ts
invite(orgId: string, input: {
emails: string[]
orgRole?: "admin" | "member"
workspaceId?: string
workspaceRole?: "admin" | "member"
}): Promise<{ data: InvitationDelivery[] }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "組織の ID です。" },
emails: { type: 'string[]', required: true, description: "招待するアドレスです。" },
orgRole: { type: '"admin" | "member"', description: "組織でのロールです。" },
workspaceId: { type: 'string', description: "承認したときに、その人を追加するワークスペースです。" },
workspaceRole: { type: '"admin" | "member"', description: "そのワークスペースでのロールです。" },
}}
/>

```ts
const { data: rows } = await client.organizations.invite(orgId, {
emails: ["teacher@school.example"],
workspaceId,
workspaceRole: "admin",
})
for (const row of rows) {
if (row.status !== "sent") showInviteLink(row.email, row.link) // not emailed: share the link yourself
}
```

**リンクを表示してください。**`status` が `sent` ではない行には、代わりに `link` が付きます。インストール環境にメールプロバイダーがないか、配信が失敗したためです。どちらの場合も招待自体は存在しますが、リンクがなければ誰もそこに到達できません。招待は 14 日で期限切れになります。組織の 1 日あたりの招待の上限に達すると、`RateLimitedError` が返されます。

### 招待の管理
招待を一覧表示、取り消し、再送信します。

```ts
listInvitations(orgId: string, opts?: { status?: "open" | "accepted" | "revoked" | "expired"; workspaceId?: string; cursor?: string; limit?: number }): Promise<OrgPage<InvitationView>>
revokeInvitation(id: string): Promise<{ data: { id: string; revoked: boolean } }>
resendInvitation(id: string): Promise<{ data: InvitationDelivery & { id: string } }>
```

<TypeTable
type={{
orgId: { type: 'string', description: "listInvitations：組織の ID です。" },
id: { type: 'string', description: "revokeInvitation と resendInvitation：招待の ID です。" },
status: { type: '"open" | "accepted" | "revoked" | "expired"', description: "listInvitations：この状態の招待だけです。" },
workspaceId: { type: 'string', description: "listInvitations：このワークスペースへの招待だけです。" },
cursor: { type: 'string', description: "listInvitations：前のページの nextCursor です。" },
limit: { type: 'number', description: "listInvitations：ページのサイズです。" },
}}
/>

```ts
const { data: open } = await client.organizations.listInvitations(orgId, { status: "open" })
await client.organizations.resendInvitation(open[0].id)
```

### 招待のプレビューと承認
`previewInvitation()` は**公開**されています。招待された人がまだログインしていない状態でも動作し、アドレスをマスクして返します。`acceptInvitation()` には、招待のものと一致するメールアドレスを持つ、ログイン済みの呼び出し元が必要です。

```ts
previewInvitation(token: string): Promise<{ data: InvitationPreview }>
acceptInvitation(token: string): Promise<{ data: { orgId: string; workspaceId: string | null } }>
```

<TypeTable
type={{
token: { type: 'string', required: true, description: "招待のリンクからのトークンです。" },
}}
/>

```ts
const { data: preview } = await client.organizations.previewInvitation(token)
const { data: joined } = await signedInClient.organizations.acceptInvitation(token)
```

承認は、`400 invitation_expired`、`invitation_revoked`、`invitation_accepted`、`email_mismatch` で失敗するほか、組織が特定のメールドメインだけを受け入れる場合は `ForbiddenError` で失敗します。

### organizations.audit(orgId, opts?)
組織の監査ログを、新しい順に読み取ります（`GET /v1/orgs/:id/audit`）。組織が停止されている間も、読み取り可能なままです。

```ts
audit(orgId: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<OrgAuditEntry>>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "組織の ID です。" },
cursor: { type: 'string', description: "前のページの nextCursor です。" },
limit: { type: 'number', description: "ページのサイズです。" },
}}
/>

```ts
const { data: entries } = await client.organizations.audit(orgId, { limit: 50 })
for (const e of entries) console.log(e.createdAt, e.action, e.actor?.displayName ?? "system")
```

各エントリーには、`id`、`workspaceId`、`action`、`targetType`、`targetId`、`details`、`createdAt`、`actor` があり、`actor` はシステムが行った操作では `null` です。`action` は**オープンな**リストです。知っている操作は表示し、それ以外は生の文字列を表示してください。固定リストだけを扱うコードは、最初の新しい操作で壊れます。

### 組織の使用状況
オーナーと組織の管理者向けの、日付範囲を指定したクレジットの使用状況レポートです（`GET /v1/orgs/:id/usage`）。`usage()` はレポートをグループ化し、`usageRows()` はその背後にある実行を新しい順にページ単位で返し、`usageCsv()` はレポートまたは行を CSV テキストとして返します。

```ts
usage(orgId: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: "workspace" | "member" | "model" | "day"; workspaceId?: string; userId?: string }): Promise<{ data: UsageReport }>
usageRows(orgId: string, opts?: { from?: string; to?: string; tz?: string; workspaceId?: string; userId?: string; cursor?: string; limit?: number }): Promise<OrgPage<UsageLogEntry>>
usageCsv(orgId: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: string; workspaceId?: string; userId?: string }): Promise<string>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "組織の ID です。" },
from: { type: 'string', description: "範囲の最初の日で、その日を含みます。" },
to: { type: 'string', description: "範囲の最後の日で、その日を含みます。範囲は最大 366 日です。" },
tz: { type: 'string', description: "日付の区切りに使う IANA タイムゾーンで、たとえば Europe/Rome です。" },
groupBy: { type: '"workspace" | "member" | "model" | "day"', description: "usage と usageCsv：レポートのグループ化の方法です。" },
workspaceId: { type: 'string', description: "1 つのワークスペースだけです。" },
userId: { type: 'string', description: "1 人のメンバーだけです。" },
cursor: { type: 'string', description: "usageRows：前のページの nextCursor です。" },
limit: { type: 'number', description: "usageRows：ページのサイズです。" },
}}
/>

```ts
const { data: report } = await client.organizations.usage(orgId, {
from: "2026-09-01",
to: "2026-09-30",
tz: "Europe/Rome",
groupBy: "workspace",
})
const csv = await client.organizations.usageCsv(orgId, { from: "2026-09-01", to: "2026-09-30" })
```

各行は、3 つのクレジットの数値を報告します。`credits` は、それまでに実行にかかった金額です。終わった実行では確定した金額、進行中の実行では保持されている確保分です。`settledCredits` と `inFlightCredits` は、その内訳です。合計は、グループ化が `truncated` の場合でも、範囲全体をカバーします。

- `platformAbsorbedCredits` は、ワークスペースの残りの予算を超えた従量制の実行の超過分で、プラットフォームが負担します。
- `chargedToBudget` は `settledCredits` から `platformAbsorbedCredits` を引いたもので、予算に反映された確定済みのクレジットです。
- `appMarkupAbsorbedCredits` は、予算でまかなえなかったアプリの上乗せ分です。レポート上に対応する実行がないため、`chargedToBudget` には含まれません。

CSV のエクスポートは、ユーザーごとに 1 分あたり 10 回までに制限されます。

## client.workspaces
### workspaces.list()
自分が属するワークスペースと、自分の `lastWorkspaceId` を一覧表示します（`GET /v1/workspaces`）。返されるのはサマリーで、`client.me()` が持つのと同じ一覧です。

```ts
list(): Promise<{ data: WorkspaceSummary[]; lastWorkspaceId: string | null }>
```

```ts
const { data: workspaces, lastWorkspaceId } = await client.workspaces.list()
```

### workspaces.listForOrg(orgId, opts?)
1 つの組織のワークスペースを一覧表示します（`GET /v1/orgs/:id/workspaces`）。

```ts
listForOrg(orgId: string, opts?: { includeArchived?: boolean }): Promise<{ data: WorkspaceView[] }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "組織の ID です。" },
includeArchived: { type: 'boolean', default: 'false', description: "アーカイブされたワークスペースも含めます。" },
}}
/>

```ts
const { data: classes } = await client.workspaces.listForOrg(orgId)
```

### workspaces.get(id)
1 つのワークスペースの完全な情報を読み取ります（`GET /v1/workspaces/:id`）。

```ts
get(id: string): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークスペースの ID です。" },
}}
/>

```ts
const { data: workspace } = await client.workspaces.get(workspaceId)
```

### workspaces.create(orgId, input)
組織の中にワークスペースを作成します（`POST /v1/orgs/:id/workspaces`）。組織が active である必要があります。

```ts
create(orgId: string, input: { name: string; slug?: string; description?: string; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
orgId: { type: 'string', required: true, description: "組織の ID です。" },
name: { type: 'string', required: true, description: "ワークスペースの名前です。" },
slug: { type: 'string', description: "一意の短い名前です。" },
description: { type: 'string', description: "説明です。" },
settings: { type: 'WorkspaceSettings', description: "組織の設定を上書きする設定です。" },
}}
/>

```ts
const { data: classOne } = await client.workspaces.create(orgId, { name: "Class 1" })
```

### workspaces.update(id, input)
ワークスペースを変更します（`PATCH /v1/workspaces/:id`）。

```ts
update(id: string, input: { name?: string; description?: string | null; settings?: WorkspaceSettings }): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークスペースの ID です。" },
name: { type: 'string', description: "新しい名前です。" },
description: { type: 'string | null', description: "新しい説明で、削除する場合は null です。" },
settings: { type: 'WorkspaceSettings', description: "変更する設定です。" },
}}
/>

```ts
await client.workspaces.update(workspaceId, { settings: { members_can_create_projects: true } })
```

### workspaces.setArchived(id, archived)
ワークスペースをアーカイブするか、アーカイブを解除します（`POST /v1/workspaces/:id/archive` または `/unarchive`）。アーカイブは元に戻せて、何も破棄されません。ワークスペースは新しい作業を受け付けなくなりますが、完全に読み取り可能なままです。アーカイブされたワークスペースへの書き込みは、`ForbiddenError` で失敗します。

```ts
setArchived(id: string, archived: boolean): Promise<{ data: WorkspaceView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークスペースの ID です。" },
archived: { type: 'boolean', required: true, description: "true でアーカイブし、false でアーカイブを解除します。" },
}}
/>

```ts
await client.workspaces.setArchived(workspaceId, true)
```

### ワークスペースのメンバー
ワークスペースのメンバーを一覧表示、追加、変更、削除します（`/v1/workspaces/:id/members`）。追加するには、その人がすでに組織に属している必要があります。新しい人を迎えるには、招待してください。組織のオーナーと管理者は、一覧に載らなくても、すべてのワークスペースの管理者です。

```ts
listMembers(id: string, opts?: { cursor?: string; limit?: number }): Promise<OrgPage<WorkspaceMemberView>>
addMember(id: string, input: { userId: string; role: "admin" | "member" }): Promise<{ data: WorkspaceMemberView }>
updateMember(id: string, userId: string, input: { role?: "admin" | "member"; status?: "active" | "suspended"; creditCap?: number | null }): Promise<{ data: WorkspaceMemberView }>
removeMember(id: string, userId: string): Promise<{ data: { removed: boolean } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークスペースの ID です。" },
userId: { type: 'string', description: "メンバーです。addMember、updateMember、removeMember で必須です。" },
role: { type: '"admin" | "member"', description: "ワークスペースでのロールです。" },
status: { type: '"active" | "suspended"', description: "updateMember：このワークスペースでメンバーを停止するか、復帰させます。" },
creditCap: { type: 'number | null', description: "updateMember：このメンバーのクレジット上限で、上限なしの場合は null です。" },
cursor: { type: 'string', description: "listMembers：前のページの nextCursor です。" },
limit: { type: 'number', description: "listMembers：ページのサイズです。" },
}}
/>

```ts
await client.workspaces.addMember(workspaceId, { userId, role: "member" })
await client.workspaces.updateMember(workspaceId, userId, { creditCap: 500 })
```

組織のアクティブなメンバーではない人を追加すると、`400 not_org_member` で失敗します。同じ人を 2 回追加すると、`409 already_a_member` で失敗します。

### 参加コード
参加コードを使うと、招待なしでワークスペースに参加できます。読み取りと変更ができるのは、ワークスペースの管理者だけです。

```ts
getJoinCode(id: string): Promise<{ data: JoinCodeView | null }>
actOnJoinCode(id: string, action: "rotate" | "enable" | "disable"): Promise<{ data: JoinCodeView }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークスペースの ID です。" },
action: { type: '"rotate" | "enable" | "disable"', description: "actOnJoinCode：rotate は新しいコードを作り、古いコードを即座に停止します。enable と disable は、コードのオンとオフを切り替えます。" },
}}
/>

```ts
const { data: code } = await client.workspaces.actOnJoinCode(workspaceId, "enable")
console.log(code.code) // for example BCDFGHJK
```

コードがまだ作られていない場合、`getJoinCode()` は `null` を返します。

### workspaces.join(code)
参加コードで、ワークスペースに参加します（`POST /v1/workspaces/join`）。自分のクライアントがどのワークスペースで操作していても動作するため、古くなった選択がこれを妨げることはありません。

```ts
join(code: string): Promise<{ data: { orgId: string; workspaceId: string } }>
```

<TypeTable
type={{
code: { type: 'string', required: true, description: "参加コードです。" },
}}
/>

```ts
const { data: joined } = await client.workspaces.join("BCDFGHJK")
```

不明なコード、無効化されたコード、アーカイブされたワークスペースのコードは、いずれも `400 join_code_invalid` で失敗します。3 つとも同じ応答なので、コードから何が存在するかがわかることはありません。試行が多すぎる場合は `RateLimitedError` が返されます。

### ワークスペースの使用状況
組織と同じクレジットレポートを、1 つのワークスペースについて返します（`GET /v1/workspaces/:id/usage`）。メンバーには自分の実行だけが見え、管理者には全員の実行が見え、`userId` で絞り込めます。

```ts
usage(id: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: "member" | "model" | "day"; userId?: string }): Promise<{ data: UsageReport }>
usageRows(id: string, opts?: { from?: string; to?: string; tz?: string; userId?: string; cursor?: string; limit?: number }): Promise<OrgPage<UsageLogEntry>>
usageCsv(id: string, opts?: { from?: string; to?: string; tz?: string; groupBy?: string; userId?: string }): Promise<string>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークスペースの ID です。" },
from: { type: 'string', description: "最初の日で、その日を含みます。" },
to: { type: 'string', description: "最後の日で、その日を含みます。" },
tz: { type: 'string', description: "IANA タイムゾーンです。" },
groupBy: { type: '"member" | "model" | "day"', description: "レポートのグループ化の方法です。" },
userId: { type: 'string', description: "管理者のみ：1 人のメンバーです。" },
cursor: { type: 'string', description: "usageRows：前のページの nextCursor です。" },
limit: { type: 'number', description: "usageRows：ページのサイズです。" },
}}
/>

```ts
const { data: byModel } = await client.workspaces.usage(workspaceId, { groupBy: "model" })
```

## エラー
すべての権限はサーバーが決め、エラーで答えます。SDK では、403 は `ForbiddenError` に、404 は `NotFoundError` に、429 は `RateLimitedError` になります。理由は、その `message` を読んでください。それ以外のステータスは、サーバーの `code` を持つ `NodaroError` として届きます。

| ステータス | コード | 発生するとき |
| --- | --- | --- |
| 400 | `validation_error` | フィールドが無効、カーソルの形式が不正、または使用状況レポートの日付、タイムゾーン、範囲が不正です |
| 400 | `terms_required` | `acceptTerms: true` なしで学校が作成されました |
| 400 | `not_org_member` | ワークスペースに追加された人が、その組織のアクティブなメンバーではありません |
| 400 | `token_workspace_mismatch` | 1 つのワークスペースに紐付けたトークンが、別のワークスペースで使われました |
| 400 | `join_code_invalid` | 参加コードが存在しない、無効になっている、またはアーカイブされたワークスペースのものです |
| 400 | `invitation_expired`、`invitation_revoked`、`invitation_accepted`、`email_mismatch` | 招待を承認できません |
| 403 | | ロールが不足している、メンバーが停止されている、組織が active ではない、ワークスペースがアーカイブされている、またはメールドメインが許可されていません |
| 404 | | その組織、ワークスペース、メンバー、招待が存在しないか、自分がそのメンバーではありません |
| 409 | `name_taken` | スラッグが使用中です |
| 409 | `already_a_member` | その人はすでにワークスペースにいます |
| 409 | `owner_cannot_leave` | オーナーが脱退しようとしました |
| 409 | `has_active_workspaces` | 組織に、アーカイブされていないワークスペースがまだあります |
| 429 | | 組織の作成、参加の試行、CSV エクスポート、招待が多すぎます |
| 503 | `billing_unavailable` | このインスタンスでは、使用状況レポートがまだ利用できません |
| 503 | `audit_unavailable` | CSV エクスポートを監査ログに記録できませんでした。もう一度試してください。 |

## Frequently asked questions

### 組織とワークスペースは、どう違いますか？

組織は、学校やチームのことで、1 人のオーナー、管理者、メンバーがいます。ワークスペースは、その中でメンバーが一緒に作業する場所で、クラスやプロジェクトチームなどです。どのワークスペースも、1 つの組織に属します。

### ワークスペースの中でワークフローを実行するには、どうすればよいですか？

client.withWorkspace(workspaceId) で、そのワークスペース用のクライアントを作成し、その上で run を呼び出します。元のクライアントは、引き続き自分の個人スペースで動作します。

### 新しい組織が pending なのは、なぜですか？

新しい組織は pending から始まり、Nodaro が承認すると active になります。それまでは、オーナーだけがそれを見ることができ、中身は何も変更できません。

### 招待がリンクとともに返ってきたときは、どうすればよいですか？

そのリンクを、招待を送った本人に見せてください。status が sent ではない行は、メールが送られていません。インストール環境にメールプロバイダーがないか、配信が失敗したためです。招待自体は存在し、そのリンクが、そこに到達する唯一の方法です。
