@ryusenkei/legal 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RYUSENKEI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # @ryusenkei/legal
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Builds a privacy policy, a notice under Japan's Act on Specified Commercial Transactions (`特定商取引法に基づく表記`), and the operator details for a footer from a short declaration. The output is structured data (articles, paragraphs, lists, tables) in Japanese; rendering is yours.
4
+
5
+ > The standard wording is a starting point, not legal advice. Have it reviewed before you publish.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @ryusenkei/legal
11
+ ```
12
+
13
+ ## Operator
14
+
15
+ ```ts
16
+ import type { Operator } from '@ryusenkei/legal';
17
+
18
+ const operator: Operator = {
19
+ name: 'Example',
20
+ url: 'https://www.example.com/',
21
+ status: 'registered', // or 'preparing'
22
+ legalName: { disclosure: 'on_request' },
23
+ address: { disclosure: 'on_request' },
24
+ representative: { disclosure: 'public', value: '...' },
25
+ email: 'hi@example.com',
26
+ };
27
+ ```
28
+
29
+ - Each detail is either `public` with a value, or `on_request` without one; the latter is shown as `ご請求があれば、遅滞なくお知らせします` (disclosed on request). Because an `on_request` detail carries no value, the declaration can live in a public repository without a personal name.
30
+ - `operatorRows(operator)` gives the table rows. `copyrightLine(operator, year)` gives the footer line.
31
+
32
+ ## Privacy policy
33
+
34
+ Declare what you collect and which services see it:
35
+
36
+ ```ts
37
+ import { privacyPolicy, retentionDaysOf } from '@ryusenkei/legal';
38
+
39
+ const declaration = {
40
+ subject: 'Example のウェブサイト',
41
+ operator,
42
+ enactedOn: '2026-11-07', // null while drafting
43
+ collected: [{ name: 'お問い合わせの内容', method: 'お問い合わせのフォーム', purposes: ['お問い合わせへの対応'], retention: { days: 365 } }],
44
+ services: [{ name: 'Turnstile', provider: 'Cloudflare, Inc.', country: '米国', role: 'transmission', purpose: '不正な送信の防止', items: ['端末と操作の情報'] }],
45
+ cookies: { analytics: [] },
46
+ } satisfies PrivacyDeclaration;
47
+
48
+ const doc = privacyPolicy(declaration);
49
+ const retentionDays = retentionDaysOf(declaration, 'お問い合わせの内容'); // 365, for the deletion job
50
+ ```
51
+
52
+ What the declaration drives:
53
+
54
+ | Declared | Generated |
55
+ | --- | --- |
56
+ | `collected` | The items with their method and purposes, and a retention article (`{ days }`, or `{ until: 'request' }`). Multiples of 365 days are written in years |
57
+ | `services` with `role: 'processor'` | An outsourcing (`委託`) article |
58
+ | `services` with `role: 'transmission'` | What is sent, to whom, and why (external transmission from the user's device) |
59
+ | any service outside 日本 | An article on provision to third parties in foreign countries, and a security-measure line about understanding those countries' rules |
60
+ | `cookies` | The purposes and services that use cookies, or a statement that no cookies or external analytics are used |
61
+ | `revisions` | `revisedOn` (the latest) and the history |
62
+
63
+ The standard articles (disclosure, correction and suspension, security measures, contact with the operator table, changes) are always included.
64
+
65
+ Drafting:
66
+
67
+ - With `enactedOn: null`, undecided items are allowed and listed in `doc.missing`: the operator's legal name, address, and representative; retention periods; service countries.
68
+ - Setting `enactedOn` while anything is still missing throws `LegalDeclarationError`.
69
+
70
+ Keep the retention in the declaration and read it with `retentionDaysOf` in the deletion job, so the policy text and the actual deletion never disagree.
71
+
72
+ ## 特定商取引法に基づく表記
73
+
74
+ ```ts
75
+ import { tokushoNotice } from '@ryusenkei/legal';
76
+
77
+ const notice = tokushoNotice({
78
+ operator: { ...operator, phone: { disclosure: 'on_request' } },
79
+ finalConfirmationScreen: false,
80
+ price: { disclosure: 'on_request' },
81
+ extraFees: '...', paymentMethods: ['クレジットカード'], paymentTiming: '...', delivery: '...', returns: '...',
82
+ updatedOn: '2026-11-07',
83
+ });
84
+ ```
85
+
86
+ - When any detail is `on_request`, the notice ends with a `表示の省略について` (omitted items) row. Omission relies on the proviso of Article 11.
87
+ - With `finalConfirmationScreen: true`, the site has a final order-confirmation screen, so Article 12-6 requires every item to be shown:
88
+ - `price` must be `public` (enforced by the type).
89
+ - An `on_request` seller name, address, or phone throws `LegalDeclarationError`.
90
+ - A missing seller name, address, phone, or price also throws.
91
+
92
+ Terms of use are not generated; they differ too much between products.
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,49 @@
1
+ /** 段落の部品(文字列と、メールアドレス・URL のリンク) */
2
+ export type Inline = string | Readonly<{
3
+ email: string;
4
+ }> | Readonly<{
5
+ url: string;
6
+ text: string;
7
+ }>;
8
+ /** 段落(文字列か、部品の列) */
9
+ export type Paragraph = string | readonly Inline[];
10
+ /** 表の行(運営者の表など) */
11
+ export type Row = Readonly<{
12
+ label: string;
13
+ value: Paragraph;
14
+ }>;
15
+ /**
16
+ * 条。番号の付け方と記号は描画の側が決める
17
+ * - `items`: 番号付きの列挙、`bullets`: 番号なしの列挙、`after`: 列挙の後の段落
18
+ */
19
+ export type Article = Readonly<{
20
+ title: string;
21
+ paragraphs: readonly Paragraph[];
22
+ items?: readonly Paragraph[];
23
+ bullets?: readonly Paragraph[];
24
+ after?: readonly Paragraph[];
25
+ table?: readonly Row[];
26
+ }>;
27
+ export type Revision = Readonly<{
28
+ date: string;
29
+ note?: string;
30
+ }>;
31
+ /**
32
+ * 組み立てた文書(見た目は持たない)
33
+ * - `missing`: 下書きの間に未定のままの項目(制定の前に埋める)
34
+ */
35
+ export type LegalDocument = Readonly<{
36
+ title: string;
37
+ intro: readonly Paragraph[];
38
+ articles: readonly Article[];
39
+ enactedOn: string | null;
40
+ revisedOn: string | undefined;
41
+ revisions: readonly Revision[];
42
+ missing: readonly string[];
43
+ }>;
44
+ /** 宣言の誤り(制定したのに未定の項目がある、特商法で省略できない項目を省略した、など) */
45
+ export declare class LegalDeclarationError extends Error {
46
+ readonly name = "LegalDeclarationError";
47
+ readonly problems: readonly string[];
48
+ constructor(message: string, problems: readonly string[]);
49
+ }
@@ -0,0 +1,9 @@
1
+ /** 宣言の誤り(制定したのに未定の項目がある、特商法で省略できない項目を省略した、など) */
2
+ export class LegalDeclarationError extends Error {
3
+ name = 'LegalDeclarationError';
4
+ problems;
5
+ constructor(message, problems) {
6
+ super(`${message}: ${problems.join('、')}`);
7
+ this.problems = problems;
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ export { LegalDeclarationError, type Article, type Inline, type LegalDocument, type Paragraph, type Revision, type Row } from './document.ts';
2
+ export { copyrightLine, ON_REQUEST_TEXT, operatorRows, type Disclosure, type Operator } from './operator.ts';
3
+ export { privacyPolicy, retentionDaysOf, retentionText, type CollectedItem, type ExternalService, type PrivacyDeclaration, type Retention } from './privacy.ts';
4
+ export { tokushoNotice, type TokushoDeclaration, type TokushoNotice } from './tokusho.ts';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { LegalDeclarationError } from "./document.js";
2
+ export { copyrightLine, ON_REQUEST_TEXT, operatorRows } from "./operator.js";
3
+ export { privacyPolicy, retentionDaysOf, retentionText } from "./privacy.js";
4
+ export { tokushoNotice } from "./tokusho.js";
@@ -0,0 +1,36 @@
1
+ import type { Row } from './document.ts';
2
+ /**
3
+ * 運営者の項目の公開の仕方
4
+ * - `public`: 値を表示する
5
+ * - `on_request`: 表示せず、請求があれば知らせる。値を持たないので、公開のリポにも置ける
6
+ */
7
+ export type Disclosure = Readonly<{
8
+ disclosure: 'public';
9
+ value: string;
10
+ }> | Readonly<{
11
+ disclosure: 'on_request';
12
+ }>;
13
+ /** 運営者(会社の値。platform の `company` から渡す) */
14
+ export type Operator = Readonly<{
15
+ /** 表示名(屋号、サービス名) */
16
+ name: string;
17
+ url?: string;
18
+ /** 登記の状態。設立の準備中は正式な名称の代わりに「設立準備中」と出す */
19
+ status: 'registered' | 'preparing';
20
+ /** 正式な名称(商号)。未定なら下書きの間だけ省略できる */
21
+ legalName?: Disclosure;
22
+ representative?: Disclosure;
23
+ address?: Disclosure;
24
+ phone?: Disclosure;
25
+ /** 窓口のメールアドレス(公開) */
26
+ email: string;
27
+ }>;
28
+ export declare const ON_REQUEST_TEXT = "\u3054\u8ACB\u6C42\u304C\u3042\u308C\u3070\u3001\u9045\u6EDE\u306A\u304F\u304A\u77E5\u3089\u305B\u3057\u307E\u3059";
29
+ export declare const isOnRequest: (d: Disclosure | undefined) => boolean;
30
+ /**
31
+ * 運営者の表の行(プライバシーポリシーの窓口、会社の情報の欄)
32
+ * - 未定の項目は行を出さない
33
+ */
34
+ export declare function operatorRows(operator: Operator): Row[];
35
+ /** フッターの著作権の表示 */
36
+ export declare const copyrightLine: (operator: Operator, year: number) => string;
@@ -0,0 +1,24 @@
1
+ export const ON_REQUEST_TEXT = 'ご請求があれば、遅滞なくお知らせします';
2
+ export const isOnRequest = (d) => d?.disclosure === 'on_request';
3
+ const show = (d) => (d.disclosure === 'public' ? d.value : ON_REQUEST_TEXT);
4
+ /**
5
+ * 運営者の表の行(プライバシーポリシーの窓口、会社の情報の欄)
6
+ * - 未定の項目は行を出さない
7
+ */
8
+ export function operatorRows(operator) {
9
+ const rows = [{ label: '運営', value: operator.url ? [{ url: operator.url, text: operator.name }] : operator.name }];
10
+ if (operator.legalName)
11
+ rows.push({ label: '名称', value: show(operator.legalName) });
12
+ else if (operator.status === 'preparing')
13
+ rows.push({ label: '名称', value: '設立準備中' });
14
+ if (operator.address)
15
+ rows.push({ label: '所在地', value: show(operator.address) });
16
+ if (operator.representative)
17
+ rows.push({ label: '代表者', value: show(operator.representative) });
18
+ if (operator.phone)
19
+ rows.push({ label: '電話番号', value: show(operator.phone) });
20
+ rows.push({ label: '窓口', value: [{ email: operator.email }] });
21
+ return rows;
22
+ }
23
+ /** フッターの著作権の表示 */
24
+ export const copyrightLine = (operator, year) => `© ${year} ${operator.name}`;
@@ -0,0 +1,62 @@
1
+ import { type LegalDocument, type Revision } from './document.ts';
2
+ import { type Operator } from './operator.ts';
3
+ /** 保管期間。日数は削除の処理からも `retentionDaysOf` で参照する */
4
+ export type Retention = Readonly<{
5
+ days: number;
6
+ }> | Readonly<{
7
+ until: 'request';
8
+ }>;
9
+ /** 集める情報 */
10
+ export type CollectedItem = Readonly<{
11
+ /** 情報の名前(例: 「お問い合わせの内容」) */
12
+ name: string;
13
+ /** 取得の方法(例: 「お問い合わせのフォーム」) */
14
+ method: string;
15
+ purposes: readonly string[];
16
+ /** 未定なら下書きの間だけ省略できる */
17
+ retention?: Retention;
18
+ }>;
19
+ /**
20
+ * 外部のサービス
21
+ * - `processor`: 取扱いの委託(サーバー、メールの配信など)
22
+ * - `transmission`: 利用者の端末から外部へ送られる情報(解析、広告、bot の判定などのタグ)
23
+ */
24
+ export type ExternalService = Readonly<{
25
+ name: string;
26
+ provider: string;
27
+ /** 事業者の国(例: 「米国」)。日本以外なら外国にある第三者への提供の条文を出す。未定なら下書きの間だけ省略できる */
28
+ country?: string;
29
+ role: 'processor' | 'transmission';
30
+ purpose: string;
31
+ /** 扱う、または送られる情報 */
32
+ items: readonly string[];
33
+ }>;
34
+ export type PrivacyDeclaration = Readonly<{
35
+ /** 対象(例: 「〇〇のウェブサイト」) */
36
+ subject: string;
37
+ /** 対象の呼び方(既定は「当サイト」) */
38
+ subjectShort?: string;
39
+ operator: Operator;
40
+ /** 制定日(YYYY-MM-DD)。`null` は下書きで、未定の項目を `missing` に出す */
41
+ enactedOn: string | null;
42
+ /** 改定の履歴(新しい順) */
43
+ revisions?: readonly Revision[];
44
+ collected: readonly CollectedItem[];
45
+ services?: readonly ExternalService[];
46
+ /** Cookie を使う目的ごとのサービス名。どれもなければ「使用していません」と出す */
47
+ cookies?: Readonly<{
48
+ analytics?: readonly string[];
49
+ advertising?: readonly string[];
50
+ affiliate?: readonly string[];
51
+ }>;
52
+ }>;
53
+ /** 保管期間の表示(365日の倍数は年、それ以外は日) */
54
+ export declare function retentionText(retention: Retention): string;
55
+ /** 削除の処理が参照する保管期間(日)。日数でない、または未定なら `undefined` */
56
+ export declare function retentionDaysOf(declaration: Pick<PrivacyDeclaration, 'collected'>, name: string): number | undefined;
57
+ /**
58
+ * プライバシーポリシーを組み立てる
59
+ * - 外部のサービスに日本以外の国があれば、外国にある第三者への提供の条文と、安全管理措置の外的環境の把握を出す
60
+ * - 制定日があるのに未定の項目が残っていれば `LegalDeclarationError`
61
+ */
62
+ export declare function privacyPolicy(d: PrivacyDeclaration): LegalDocument;
@@ -0,0 +1,137 @@
1
+ // プライバシーポリシーを宣言から組み立てる(日本語、敬体、前文と第 N 条の形)
2
+ // 定型の文面は公開の前に人が確認する(文面の正しさは、このパッケージでは保証しない)
3
+ import { LegalDeclarationError } from "./document.js";
4
+ import { operatorRows } from "./operator.js";
5
+ const JAPAN = '日本';
6
+ /** 保管期間の表示(365日の倍数は年、それ以外は日) */
7
+ export function retentionText(retention) {
8
+ if ('until' in retention)
9
+ return '利用者からの削除のお申し出まで';
10
+ const { days } = retention;
11
+ return days % 365 === 0 ? `${days / 365}年間` : `${days}日間`;
12
+ }
13
+ /** 削除の処理が参照する保管期間(日)。日数でない、または未定なら `undefined` */
14
+ export function retentionDaysOf(declaration, name) {
15
+ const retention = declaration.collected.find((c) => c.name === name)?.retention;
16
+ return retention && 'days' in retention ? retention.days : undefined;
17
+ }
18
+ function missingOf(d) {
19
+ const missing = [];
20
+ if (!d.operator.legalName && d.operator.status === 'registered')
21
+ missing.push('operator.legalName');
22
+ if (!d.operator.address)
23
+ missing.push('operator.address');
24
+ if (!d.operator.representative)
25
+ missing.push('operator.representative');
26
+ d.collected.forEach((c, i) => void (!c.retention && missing.push(`collected.${i}.retention`)));
27
+ (d.services ?? []).forEach((s, i) => void (!s.country && missing.push(`services.${i}.country`)));
28
+ return missing;
29
+ }
30
+ /**
31
+ * プライバシーポリシーを組み立てる
32
+ * - 外部のサービスに日本以外の国があれば、外国にある第三者への提供の条文と、安全管理措置の外的環境の把握を出す
33
+ * - 制定日があるのに未定の項目が残っていれば `LegalDeclarationError`
34
+ */
35
+ export function privacyPolicy(d) {
36
+ const missing = missingOf(d);
37
+ if (d.enactedOn && missing.length)
38
+ throw new LegalDeclarationError('制定したプライバシーポリシーに未定の項目がある', missing);
39
+ const site = d.subjectShort ?? '当サイト';
40
+ const services = d.services ?? [];
41
+ const processors = services.filter((s) => s.role === 'processor');
42
+ const transmissions = services.filter((s) => s.role === 'transmission');
43
+ const foreign = services.filter((s) => s.country && s.country !== JAPAN);
44
+ const countries = [...new Set(foreign.map((s) => s.country))];
45
+ const cookieUses = Object.entries({ アクセスの解析: d.cookies?.analytics, 広告の配信: d.cookies?.advertising, アフィリエイトの成果の計測: d.cookies?.affiliate }).filter(([, names]) => names?.length);
46
+ const at = (s) => `${s.provider}${s.country ? `(${s.country})` : ''}`;
47
+ const articles = [
48
+ {
49
+ title: '取得する情報と利用目的',
50
+ paragraphs: [`当社は、${site}で以下の情報を取得し、それぞれの目的で利用します。`],
51
+ bullets: d.collected.map((c) => `${c.name}(${c.method}): ${c.purposes.join('、')}`),
52
+ after: ['当社は、利用者の同意を得ることなく、上記の目的の範囲を超えて個人情報等を利用しません。'],
53
+ },
54
+ {
55
+ title: '保管期間',
56
+ paragraphs: ['当社は、取得した情報を以下の期間保管し、期間を過ぎた情報は遅滞なく消去します。'],
57
+ bullets: d.collected.map((c) => `${c.name}: ${c.retention ? retentionText(c.retention) : '(未定)'}`),
58
+ },
59
+ cookieUses.length || transmissions.length
60
+ ? {
61
+ title: 'Cookie と外部への情報の送信',
62
+ paragraphs: [
63
+ ...(cookieUses.length ? [`${site}は、以下の目的で Cookie を使用します。利用者は、ブラウザーの設定で Cookie を無効にできます。`] : [`${site}は、Cookie を使用していません。`]),
64
+ ],
65
+ ...(cookieUses.length && { bullets: cookieUses.map(([use, names]) => `${use}: ${names.join('、')}`) }),
66
+ ...(transmissions.length && {
67
+ after: [`${site}は、以下のサービスのために、利用者の端末から外部へ情報を送信します。`, ...transmissions.map((s) => `${s.name}(送信先: ${at(s)}): ${s.items.join('、')}を、${s.purpose}のために送信します。`)],
68
+ }),
69
+ }
70
+ : { title: 'Cookie と外部への情報の送信', paragraphs: [`${site}は、Cookie を使用していません。また、アクセスの解析や広告のために、外部のサービスへ利用者の情報を送信していません。`] },
71
+ {
72
+ title: '個人情報等の第三者提供',
73
+ paragraphs: ['当社は、以下の場合を除き、あらかじめ利用者の同意を得ることなく、個人情報等を第三者に提供しません。'],
74
+ items: [
75
+ '利用目的の達成に必要な範囲で、個人情報等の取扱いの全部または一部を委託する場合',
76
+ '合併その他の事由による事業の承継に伴って個人情報等が提供される場合',
77
+ '国の機関もしくは地方公共団体またはその委託を受けた者が法令の定める事務を遂行することに対して協力する必要がある場合であって、利用者の同意を得ることにより当該事務の遂行に支障を及ぼすおそれがあるとき',
78
+ 'その他、法令で認められる場合',
79
+ ],
80
+ },
81
+ ...(processors.length
82
+ ? [{ title: '取扱いの委託', paragraphs: ['当社は、利用目的の達成に必要な範囲で、以下の事業者に個人情報等の取扱いを委託し、委託先を適切に監督します。'], bullets: processors.map((s) => `${at(s)}: ${s.purpose}(${s.items.join('、')})`) }]
83
+ : []),
84
+ ...(foreign.length
85
+ ? [
86
+ {
87
+ title: '外国にある第三者への提供',
88
+ paragraphs: [
89
+ `当社は、${[...new Set(foreign.map((s) => `${s.country}の ${s.provider}`))].join('、')} に個人情報等の取扱いを委託し、または個人情報等を提供します。この場合、法令に従い、当該国の個人情報の保護に関する制度を把握した上で、必要な措置を講じます。講じている措置の内容は、ご請求があれば遅滞なくお知らせします。`,
90
+ ],
91
+ },
92
+ ]
93
+ : []),
94
+ {
95
+ title: '個人情報等の開示',
96
+ paragraphs: ['当社は、利用者から個人情報の保護に関する法律の定めに基づき個人情報等の開示を求められたときは、利用者ご本人からのご請求であることを確認の上で、遅滞なく開示します(開示すべき個人情報等が存在しないときは、その旨を通知します)。ただし、法令により開示の義務を負わない場合は、この限りではありません。'],
97
+ },
98
+ {
99
+ title: '個人情報等の訂正及び利用停止等',
100
+ paragraphs: [
101
+ '当社は、利用者から、個人情報等が真実でないという理由によって訂正を求められた場合、または利用目的の範囲を超えて取り扱われているという理由もしくは不正の手段により取得されたという理由によって利用の停止もしくは消去を求められた場合には、利用者ご本人からのご請求であることを確認の上で遅滞なく必要な調査を行い、その結果に基づき、訂正、利用の停止または消去を行い、その旨を利用者に通知します。',
102
+ '前項の規定は、法令により当社が訂正、利用の停止または消去の義務を負わない場合は、適用しません。',
103
+ ],
104
+ },
105
+ {
106
+ title: '安全管理措置の実施',
107
+ paragraphs: ['当社は、個人情報等の漏えい、滅失または毀損の防止のため、以下の措置を講じます。'],
108
+ bullets: [
109
+ '個人情報等の取扱いの責任者の設置と、取扱いの状況の定期的な点検',
110
+ '個人情報等を取り扱う者への秘密の保持の義務づけ',
111
+ '個人情報等を扱う権限の限定と、アクセスの制御',
112
+ '通信の暗号化',
113
+ '委託先との秘密の保持に関する取り決めと、委託先の監督',
114
+ ...(countries.length ? [`外国(${countries.join('、')})で個人情報等を取り扱う場合の、当該国の個人情報の保護に関する制度の把握`] : []),
115
+ ],
116
+ },
117
+ {
118
+ title: 'お問い合わせ先',
119
+ paragraphs: [['本ポリシーに関するご意見、ご質問、個人情報等の取扱いに関するお問い合わせは、', { email: d.operator.email }, 'までご連絡ください。']],
120
+ table: operatorRows(d.operator),
121
+ },
122
+ {
123
+ title: 'プライバシーポリシーの変更手続',
124
+ paragraphs: [`当社は、本ポリシーの内容を適宜見直し、その改善に努めます。本ポリシーは、法令に別段の定めがある場合を除き、変更することができます。変更後の本ポリシーは、${site}に掲載した時から効力を生じます。`],
125
+ },
126
+ ];
127
+ const revisions = d.revisions ?? [];
128
+ return {
129
+ title: 'プライバシーポリシー',
130
+ intro: [`当社は、${d.subject}(以下「${site}」といいます。)の運営にあたり取得した利用者の個人情報等の取扱いについて、以下のとおりプライバシーポリシー(以下「本ポリシー」といいます。)を定めます。`],
131
+ articles,
132
+ enactedOn: d.enactedOn,
133
+ revisedOn: revisions[0]?.date,
134
+ revisions,
135
+ missing,
136
+ };
137
+ }
@@ -0,0 +1,47 @@
1
+ import { type Row } from './document.ts';
2
+ import { type Disclosure, type Operator } from './operator.ts';
3
+ type Common = Readonly<{
4
+ operator: Operator;
5
+ /** 運営の責任者(任意) */
6
+ responsiblePerson?: Disclosure;
7
+ /** 商品代金以外の必要料金 */
8
+ extraFees: string;
9
+ paymentMethods: readonly string[];
10
+ paymentTiming: string;
11
+ /** 商品の引渡し、サービスの提供の時期 */
12
+ delivery: string;
13
+ /** 返品・キャンセル */
14
+ returns: string;
15
+ installments?: string;
16
+ /** 動作環境(ソフトウェア、デジタルの商品) */
17
+ environment?: string;
18
+ /** 最終の更新日(YYYY-MM-DD) */
19
+ updatedOn: string;
20
+ }>;
21
+ /**
22
+ * 特商法の表記の宣言
23
+ * - `finalConfirmationScreen: true`(サイトに申込の最終確認画面がある)なら、価格は公開が必須(型で防ぐ)。販売業者の項目は実行時に検査する
24
+ */
25
+ export type TokushoDeclaration = (Common & Readonly<{
26
+ finalConfirmationScreen: true;
27
+ price: Readonly<{
28
+ disclosure: 'public';
29
+ value: string;
30
+ }>;
31
+ }>) | (Common & Readonly<{
32
+ finalConfirmationScreen: false;
33
+ price: Disclosure;
34
+ }>);
35
+ export type TokushoNotice = Readonly<{
36
+ title: string;
37
+ rows: readonly Row[];
38
+ updatedOn: string;
39
+ omitted: boolean;
40
+ }>;
41
+ /**
42
+ * 特商法の表記を組み立てる
43
+ * - 「請求があれば開示」の項目が1つでもあれば、「表示の省略について」の行を最後に足す
44
+ * - 最終確認画面があるのに省略した項目があれば `LegalDeclarationError`
45
+ */
46
+ export declare function tokushoNotice(d: TokushoDeclaration): TokushoNotice;
47
+ export {};
@@ -0,0 +1,48 @@
1
+ // 特定商取引法に基づく表記を宣言から組み立てる
2
+ // 省略(請求があれば開示)は11条ただし書による。申込の最終確認画面を持つと12条の6で全項目の表示が要るので、省略を使えない
3
+ // 定型の文面は公開の前に人が確認する(文面の正しさは、このパッケージでは保証しない)
4
+ import { LegalDeclarationError } from "./document.js";
5
+ import { isOnRequest, ON_REQUEST_TEXT } from "./operator.js";
6
+ const show = (d) => (d.disclosure === 'public' ? d.value : ON_REQUEST_TEXT);
7
+ /**
8
+ * 特商法の表記を組み立てる
9
+ * - 「請求があれば開示」の項目が1つでもあれば、「表示の省略について」の行を最後に足す
10
+ * - 最終確認画面があるのに省略した項目があれば `LegalDeclarationError`
11
+ */
12
+ export function tokushoNotice(d) {
13
+ const o = d.operator;
14
+ // 販売業者は正式な名称(個人の事業なら氏名)。請求開示にもできる
15
+ const seller = o.legalName;
16
+ const fields = [
17
+ ['販売業者', seller],
18
+ ['所在地', o.address],
19
+ ['電話番号', o.phone],
20
+ ['運営責任者', d.responsiblePerson],
21
+ ['販売価格', d.price],
22
+ ];
23
+ const missing = fields.filter(([label, v]) => !v && label !== '運営責任者').map(([label]) => label);
24
+ if (missing.length)
25
+ throw new LegalDeclarationError('特商法の表記に必要な項目がない', missing);
26
+ const omittedLabels = fields.filter(([, v]) => isOnRequest(v)).map(([label]) => label);
27
+ if (d.finalConfirmationScreen && omittedLabels.length)
28
+ throw new LegalDeclarationError('申込の最終確認画面があると省略できない(12条の6)', omittedLabels);
29
+ const rows = [
30
+ { label: '販売業者', value: `${o.name}\n${show(seller)}` },
31
+ { label: '所在地', value: show(o.address) },
32
+ { label: '電話番号', value: show(o.phone) },
33
+ ...(d.responsiblePerson ? [{ label: '運営責任者', value: show(d.responsiblePerson) }] : []),
34
+ { label: 'メールアドレス', value: [{ email: o.email }] },
35
+ { label: '販売価格', value: show(d.price) },
36
+ { label: '商品代金以外の必要料金', value: d.extraFees },
37
+ { label: '支払方法', value: d.paymentMethods.join('\n') },
38
+ ...(d.installments ? [{ label: '分割払い', value: d.installments }] : []),
39
+ { label: '支払時期', value: d.paymentTiming },
40
+ { label: '引渡し・提供の時期', value: d.delivery },
41
+ { label: '返品・キャンセル', value: d.returns },
42
+ ...(d.environment ? [{ label: '動作環境', value: d.environment }] : []),
43
+ ];
44
+ const omitted = omittedLabels.length > 0;
45
+ if (omitted)
46
+ rows.push({ label: '表示の省略について', value: '本表記に記載のない事項、および「ご請求があれば、遅滞なくお知らせします」とした事項は、ご請求があれば、お申し込みのご判断に先立って遅滞なく、書面または電子メールで提供します。' });
47
+ return { title: '特定商取引法に基づく表記', rows, updatedOn: d.updatedOn, omitted };
48
+ }
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@ryusenkei/legal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "法務の本文(プライバシーポリシー、特定商取引法の表記、運営者の表示)を宣言から組み立てる",
5
+ "license": "MIT",
6
+ "author": "RYUSENKEI",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ryusenkei-inc/toolkit.git",
10
+ "directory": "packages/runtime/legal"
11
+ },
12
+ "homepage": "https://github.com/ryusenkei-inc/toolkit/tree/main/packages/runtime/legal",
13
+ "type": "module",
14
+ "sideEffects": false,
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json",
33
+ "typecheck": "tsc --noEmit"
34
+ }
6
35
  }