@learncard/email-templates 1.0.1 → 1.0.3
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/README.md +43 -43
- package/dist/previews/account-approved-scenarios.d.ts +2 -1
- package/dist/previews/account-approved-scenarios.d.ts.map +1 -1
- package/dist/previews/endorsement-request-scenarios.d.ts +2 -1
- package/dist/previews/endorsement-request-scenarios.d.ts.map +1 -1
- package/dist/previews/guardian-approval-scenarios.d.ts +2 -1
- package/dist/previews/guardian-approval-scenarios.d.ts.map +1 -1
- package/dist/previews/guardian-credential-scenarios.d.ts +2 -1
- package/dist/previews/guardian-credential-scenarios.d.ts.map +1 -1
- package/dist/previews/inbox-claim-scenarios.d.ts +2 -1
- package/dist/previews/inbox-claim-scenarios.d.ts.map +1 -1
- package/dist/previews/recovery-key-scenarios.d.ts +2 -1
- package/dist/previews/recovery-key-scenarios.d.ts.map +1 -1
- package/dist/previews/verification-code-scenarios.d.ts +2 -1
- package/dist/previews/verification-code-scenarios.d.ts.map +1 -1
- package/dist/templates/account-approved.d.ts +1 -1
- package/dist/templates/account-approved.d.ts.map +1 -1
- package/dist/templates/credential-awaiting-guardian.d.ts +1 -1
- package/dist/templates/credential-awaiting-guardian.d.ts.map +1 -1
- package/dist/templates/email-verification.d.ts +1 -1
- package/dist/templates/email-verification.d.ts.map +1 -1
- package/dist/templates/endorsement-request.d.ts +1 -1
- package/dist/templates/endorsement-request.d.ts.map +1 -1
- package/dist/templates/guardian-approval.d.ts +1 -1
- package/dist/templates/guardian-approval.d.ts.map +1 -1
- package/dist/templates/guardian-approved-claim.d.ts +1 -1
- package/dist/templates/guardian-approved-claim.d.ts.map +1 -1
- package/dist/templates/guardian-credential-approval.d.ts +1 -1
- package/dist/templates/guardian-credential-approval.d.ts.map +1 -1
- package/dist/templates/guardian-email-otp.d.ts +1 -1
- package/dist/templates/guardian-email-otp.d.ts.map +1 -1
- package/dist/templates/guardian-rejected-credential.d.ts +1 -1
- package/dist/templates/guardian-rejected-credential.d.ts.map +1 -1
- package/dist/templates/inbox-claim.d.ts +1 -1
- package/dist/templates/inbox-claim.d.ts.map +1 -1
- package/dist/templates/recovery-key.d.ts +1 -1
- package/dist/templates/recovery-key.d.ts.map +1 -1
- package/dist/templates/verification-code.d.ts +1 -1
- package/dist/templates/verification-code.d.ts.map +1 -1
- package/dist/templates/verification-code.js +9 -9
- package/dist/templates/verification-code.js.map +1 -1
- package/package.json +49 -39
- package/src/__tests__/render.test.ts +432 -0
- package/src/__tests__/sms.test.ts +95 -0
- package/src/__tests__/tenant-registry.test.ts +257 -0
- package/src/branding.ts +61 -0
- package/src/components/CodeBlock.tsx +61 -0
- package/src/components/EmailButton.tsx +36 -0
- package/src/components/IssuerLogo.tsx +41 -0
- package/src/components/Layout.tsx +192 -0
- package/src/components/LinkFallback.tsx +50 -0
- package/src/index.ts +50 -0
- package/src/previews/_fixtures.tsx +51 -0
- package/src/previews/account-approved-scenarios.tsx +31 -0
- package/src/previews/endorsement-request-scenarios.tsx +50 -0
- package/src/previews/guardian-approval-scenarios.tsx +39 -0
- package/src/previews/guardian-credential-scenarios.tsx +88 -0
- package/src/previews/inbox-claim-scenarios.tsx +66 -0
- package/src/previews/recovery-key-scenarios.tsx +32 -0
- package/src/previews/verification-code-scenarios.tsx +60 -0
- package/src/render.ts +409 -0
- package/src/sms.ts +68 -0
- package/src/templates/account-approved.tsx +105 -0
- package/src/templates/credential-awaiting-guardian.tsx +137 -0
- package/src/templates/email-verification.tsx +110 -0
- package/src/templates/endorsement-request.tsx +157 -0
- package/src/templates/guardian-approval.tsx +122 -0
- package/src/templates/guardian-approved-claim.tsx +114 -0
- package/src/templates/guardian-credential-approval.tsx +127 -0
- package/src/templates/guardian-email-otp.tsx +101 -0
- package/src/templates/guardian-rejected-credential.tsx +103 -0
- package/src/templates/inbox-claim.tsx +141 -0
- package/src/templates/index.ts +35 -0
- package/src/templates/recovery-key.tsx +102 -0
- package/src/templates/verification-code.tsx +164 -0
- package/src/tenant-registry.ts +204 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VerificationCode — shared template for all code-based verification emails.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - login-verification-code (lca-api firebase.ts)
|
|
6
|
+
* - recovery-email-code (lca-api keys.ts)
|
|
7
|
+
* - embed-email-verification (brain-service contact-methods.ts)
|
|
8
|
+
* - contact-method-verification (brain-service contact-methods.ts)
|
|
9
|
+
*
|
|
10
|
+
* The `variant` prop controls the subject line and descriptive text.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Text } from '@react-email/components';
|
|
14
|
+
import * as React from 'react';
|
|
15
|
+
|
|
16
|
+
import type { TenantBranding } from '../branding';
|
|
17
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
18
|
+
import { Layout } from '../components/Layout';
|
|
19
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
20
|
+
|
|
21
|
+
export type VerificationCodeVariant =
|
|
22
|
+
| 'login'
|
|
23
|
+
| 'recovery-email'
|
|
24
|
+
| 'embed-verification'
|
|
25
|
+
| 'contact-method';
|
|
26
|
+
|
|
27
|
+
export interface VerificationCodeProps {
|
|
28
|
+
branding: TenantBranding;
|
|
29
|
+
verificationCode: string;
|
|
30
|
+
verificationEmail?: string;
|
|
31
|
+
variant: VerificationCodeVariant;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const VARIANT_CONFIG: Record<
|
|
35
|
+
VerificationCodeVariant,
|
|
36
|
+
{
|
|
37
|
+
subject: (brandName: string) => string;
|
|
38
|
+
heading: (brandName: string) => string;
|
|
39
|
+
description: (email: string | undefined, brandName: string) => string;
|
|
40
|
+
expiry: string;
|
|
41
|
+
}
|
|
42
|
+
> = {
|
|
43
|
+
login: {
|
|
44
|
+
subject: b => `Your ${b} login code`,
|
|
45
|
+
heading: b => `Your ${b} Login Code`,
|
|
46
|
+
description: (email, b) =>
|
|
47
|
+
email
|
|
48
|
+
? `Here's your secure 6-digit code to log into ${b}`
|
|
49
|
+
: `Here's your secure 6-digit code to log into ${b}`,
|
|
50
|
+
expiry: 'This code will expire in 5 minutes. If you didn\u2019t request this, you can safely ignore this email.',
|
|
51
|
+
},
|
|
52
|
+
'recovery-email': {
|
|
53
|
+
subject: () => 'Verify your recovery email',
|
|
54
|
+
heading: b => `Your ${b} Verification Code`,
|
|
55
|
+
description: (email, b) =>
|
|
56
|
+
email
|
|
57
|
+
? `Here's your secure 6-digit code to add ${email} as a recovery method for ${b}`
|
|
58
|
+
: `Here's your secure 6-digit code to add a recovery method for ${b}`,
|
|
59
|
+
expiry: 'This code will expire in 5 minutes. If you didn\u2019t request this, you can safely ignore this email.',
|
|
60
|
+
},
|
|
61
|
+
'embed-verification': {
|
|
62
|
+
subject: b => `Your ${b} verification code`,
|
|
63
|
+
heading: b => `Your ${b} Verification Code`,
|
|
64
|
+
description: email =>
|
|
65
|
+
email
|
|
66
|
+
? `Enter this code to verify ${email} and claim your credential:`
|
|
67
|
+
: 'Enter this code to verify your email address and claim your credential:',
|
|
68
|
+
expiry: 'This code will expire in 10 minutes. If you didn\u2019t request this, you can safely ignore this email.',
|
|
69
|
+
},
|
|
70
|
+
'contact-method': {
|
|
71
|
+
subject: b => `Your ${b} verification code`,
|
|
72
|
+
heading: b => `Your ${b} Verification Code`,
|
|
73
|
+
description: () => 'Enter this code in the app to verify your contact information.',
|
|
74
|
+
expiry: 'This code will expire in 24 hours. If you didn\u2019t request this, you can safely ignore this email.',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const VerificationCode: React.FC<VerificationCodeProps> = ({
|
|
79
|
+
branding,
|
|
80
|
+
verificationCode,
|
|
81
|
+
verificationEmail,
|
|
82
|
+
variant,
|
|
83
|
+
}) => {
|
|
84
|
+
const config = VARIANT_CONFIG[variant];
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Layout branding={branding} preview={`Your code: ${verificationCode}`}>
|
|
88
|
+
<Text style={headingStyle}>{config.heading(branding.brandName)}</Text>
|
|
89
|
+
|
|
90
|
+
<Text style={paragraph}>Hello,</Text>
|
|
91
|
+
|
|
92
|
+
<Text style={paragraph}>
|
|
93
|
+
{config.description(verificationEmail, branding.brandName)}
|
|
94
|
+
</Text>
|
|
95
|
+
|
|
96
|
+
<CodeBlock code={verificationCode} label="Verification code" />
|
|
97
|
+
|
|
98
|
+
<Text style={muted}>{config.expiry}</Text>
|
|
99
|
+
|
|
100
|
+
<Text style={paragraph}>
|
|
101
|
+
{branding.brandName} is your private, digital passport for learning and work. It
|
|
102
|
+
lets you securely collect and share your verified skills and achievements online.
|
|
103
|
+
</Text>
|
|
104
|
+
|
|
105
|
+
<Text style={signOff}>
|
|
106
|
+
Sincerely,
|
|
107
|
+
<br />
|
|
108
|
+
The {branding.brandName} Team
|
|
109
|
+
</Text>
|
|
110
|
+
</Layout>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const getVerificationCodeSubject = (
|
|
115
|
+
branding: TenantBranding,
|
|
116
|
+
variant: VerificationCodeVariant
|
|
117
|
+
): string => VARIANT_CONFIG[variant].subject(branding.brandName);
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Styles
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
const headingStyle: React.CSSProperties = {
|
|
124
|
+
fontSize: 24,
|
|
125
|
+
fontWeight: 600,
|
|
126
|
+
color: '#111827',
|
|
127
|
+
margin: '0 0 24px',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const paragraph: React.CSSProperties = {
|
|
131
|
+
fontSize: 16,
|
|
132
|
+
color: '#374151',
|
|
133
|
+
lineHeight: '24px',
|
|
134
|
+
margin: '0 0 24px',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const muted: React.CSSProperties = {
|
|
138
|
+
fontSize: 14,
|
|
139
|
+
color: '#6b7280',
|
|
140
|
+
lineHeight: '20px',
|
|
141
|
+
margin: '0 0 24px',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const signOff: React.CSSProperties = {
|
|
145
|
+
fontSize: 14,
|
|
146
|
+
color: '#374151',
|
|
147
|
+
lineHeight: '20px',
|
|
148
|
+
margin: '24px 0 0',
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Preview (used by `bun run dev` / react-email dev server)
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
export default function Preview() {
|
|
156
|
+
return (
|
|
157
|
+
<VerificationCode
|
|
158
|
+
branding={DEFAULT_BRANDING}
|
|
159
|
+
verificationCode="847293"
|
|
160
|
+
verificationEmail="jane@example.com"
|
|
161
|
+
variant="login"
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side tenant resolution for brain-service and lca-api.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order (first match wins):
|
|
5
|
+
* 1. Explicit `X-Tenant-Id` header — native apps, any client that knows its tenant
|
|
6
|
+
* 2. `Origin` / `Referer` header hostname — web apps (e.g. alpha.vetpass.app → vetpass)
|
|
7
|
+
* 3. `DEFAULT_TENANT_ID` env var — per-tenant deploys, cron jobs, server-to-server
|
|
8
|
+
* 4. Fallback to 'learncard'
|
|
9
|
+
*
|
|
10
|
+
* The registry maps tenant IDs to email branding overrides. Missing fields
|
|
11
|
+
* fall back to LearnCard defaults via `resolveBranding()`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { TenantBranding } from './branding';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface ResolvedTenant {
|
|
21
|
+
/** Canonical tenant identifier, e.g. 'vetpass', 'learncard'. */
|
|
22
|
+
id: string;
|
|
23
|
+
|
|
24
|
+
/** Partial email branding — pass to `resolveBranding()` for full defaults. */
|
|
25
|
+
emailBranding: Partial<TenantBranding>;
|
|
26
|
+
|
|
27
|
+
/** How the tenant was resolved — useful for logging / debugging. */
|
|
28
|
+
resolvedVia: 'header' | 'origin' | 'env' | 'default';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RequestHeaders {
|
|
32
|
+
/** Value of the X-Tenant-Id header. */
|
|
33
|
+
'x-tenant-id'?: string | undefined;
|
|
34
|
+
|
|
35
|
+
/** Origin header (e.g. "https://alpha.vetpass.app"). */
|
|
36
|
+
origin?: string | undefined;
|
|
37
|
+
|
|
38
|
+
/** Referer header (fallback for Origin). */
|
|
39
|
+
referer?: string | undefined;
|
|
40
|
+
|
|
41
|
+
[key: string]: string | string[] | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Origin → tenant ID mapping
|
|
46
|
+
//
|
|
47
|
+
// Add new entries when a tenant gets a web deployment.
|
|
48
|
+
// Supports exact hostname match and wildcard subdomains.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const ORIGIN_MAP: Record<string, string> = {
|
|
52
|
+
// VetPass
|
|
53
|
+
'vetpass.app': 'vetpass',
|
|
54
|
+
'alpha.vetpass.app': 'vetpass',
|
|
55
|
+
'staging.vetpass.app': 'vetpass',
|
|
56
|
+
|
|
57
|
+
// LearnCard
|
|
58
|
+
'learncard.app': 'learncard',
|
|
59
|
+
'staging.learncard.app': 'learncard',
|
|
60
|
+
'app.learncard.com': 'learncard',
|
|
61
|
+
|
|
62
|
+
// Local dev
|
|
63
|
+
'localhost': 'learncard',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Tenant email branding registry
|
|
68
|
+
//
|
|
69
|
+
// Only override fields that differ from LearnCard defaults.
|
|
70
|
+
// Missing fields → resolveBranding() fills in DEFAULT_BRANDING.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const TENANT_EMAIL_BRANDING: Record<string, Partial<TenantBranding>> = {
|
|
74
|
+
learncard: {},
|
|
75
|
+
|
|
76
|
+
vetpass: {
|
|
77
|
+
brandName: 'VetPass',
|
|
78
|
+
logoUrl: 'https://vetpass.app/assets/icon/icon.png',
|
|
79
|
+
logoAlt: 'VetPass',
|
|
80
|
+
primaryColor: '#1B5E20',
|
|
81
|
+
primaryTextColor: '#ffffff',
|
|
82
|
+
supportEmail: 'support@vetpass.app',
|
|
83
|
+
websiteUrl: 'https://www.vetpass.app',
|
|
84
|
+
appUrl: 'https://vetpass.app',
|
|
85
|
+
fromDomain: 'vetpass.app',
|
|
86
|
+
copyrightHolder: 'VetPass',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Resolution
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract a hostname from a URL string. Returns undefined if parsing fails.
|
|
96
|
+
*/
|
|
97
|
+
const extractHostname = (urlString: string): string | undefined => {
|
|
98
|
+
try {
|
|
99
|
+
return new URL(urlString).hostname;
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Look up tenant ID from a hostname, checking exact match first,
|
|
107
|
+
* then stripping subdomains progressively (e.g. "foo.bar.vetpass.app" → "vetpass.app").
|
|
108
|
+
*/
|
|
109
|
+
const tenantIdFromHostname = (hostname: string): string | undefined => {
|
|
110
|
+
if (ORIGIN_MAP[hostname]) return ORIGIN_MAP[hostname];
|
|
111
|
+
|
|
112
|
+
// Strip subdomains: "alpha.vetpass.app" → "vetpass.app"
|
|
113
|
+
const parts = hostname.split('.');
|
|
114
|
+
|
|
115
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
116
|
+
const parent = parts.slice(i).join('.');
|
|
117
|
+
|
|
118
|
+
if (ORIGIN_MAP[parent]) return ORIGIN_MAP[parent];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return undefined;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the tenant from HTTP request headers.
|
|
126
|
+
*
|
|
127
|
+
* @param headers — Request headers object. Accepts plain objects, Node IncomingHttpHeaders, etc.
|
|
128
|
+
* @param envTenantId — Override for `process.env.DEFAULT_TENANT_ID` (useful for testing).
|
|
129
|
+
*/
|
|
130
|
+
export const resolveTenantFromRequest = (
|
|
131
|
+
headers: RequestHeaders,
|
|
132
|
+
envTenantId?: string,
|
|
133
|
+
): ResolvedTenant => {
|
|
134
|
+
// 1. Explicit X-Tenant-Id header (native apps, any informed client)
|
|
135
|
+
const explicit = normalizeHeader(headers['x-tenant-id']);
|
|
136
|
+
|
|
137
|
+
if (explicit) {
|
|
138
|
+
return buildTenant(explicit, 'header');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Origin / Referer header (web apps)
|
|
142
|
+
const origin = normalizeHeader(headers.origin) || normalizeHeader(headers.referer);
|
|
143
|
+
|
|
144
|
+
if (origin) {
|
|
145
|
+
const hostname = extractHostname(origin);
|
|
146
|
+
|
|
147
|
+
if (hostname) {
|
|
148
|
+
const matched = tenantIdFromHostname(hostname);
|
|
149
|
+
|
|
150
|
+
if (matched) {
|
|
151
|
+
return buildTenant(matched, 'origin');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Deploy-level env var
|
|
157
|
+
const envDefault = envTenantId ?? process.env.DEFAULT_TENANT_ID;
|
|
158
|
+
|
|
159
|
+
if (envDefault) {
|
|
160
|
+
return buildTenant(envDefault, 'env');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 4. Safe default
|
|
164
|
+
return buildTenant('learncard', 'default');
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Helpers
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
const normalizeHeader = (value: string | string[] | undefined): string | undefined => {
|
|
172
|
+
if (Array.isArray(value)) return value[0];
|
|
173
|
+
|
|
174
|
+
return value || undefined;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const buildTenant = (id: string, resolvedVia: ResolvedTenant['resolvedVia']): ResolvedTenant => ({
|
|
178
|
+
id,
|
|
179
|
+
emailBranding: TENANT_EMAIL_BRANDING[id] ?? {},
|
|
180
|
+
resolvedVia,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Admin helpers (for extending the registry at runtime via env vars)
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Register additional origin → tenant mappings at startup.
|
|
189
|
+
* Useful for dev/staging domains configured via env vars.
|
|
190
|
+
*/
|
|
191
|
+
export const registerOriginMapping = (hostname: string, tenantId: string): void => {
|
|
192
|
+
ORIGIN_MAP[hostname] = tenantId;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Register or update email branding for a tenant at startup.
|
|
197
|
+
* Merges with any existing branding (does not replace).
|
|
198
|
+
*/
|
|
199
|
+
export const registerTenantBranding = (tenantId: string, branding: Partial<TenantBranding>): void => {
|
|
200
|
+
TENANT_EMAIL_BRANDING[tenantId] = {
|
|
201
|
+
...TENANT_EMAIL_BRANDING[tenantId],
|
|
202
|
+
...branding,
|
|
203
|
+
};
|
|
204
|
+
};
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Learning Economy Foundation <sdk@learningeconomy.io>
|
|
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.
|